diff --git a/docs/Project.toml b/docs/Project.toml index 531318e44e..f37abc4e24 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,6 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" [compat] Documenter = "0.25" diff --git a/docs/make.jl b/docs/make.jl index 3770954715..d5ea00367b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,15 +5,24 @@ makedocs( format = Documenter.HTML( # See https://github.com/JuliaDocs/Documenter.jl/issues/868 prettyurls = get(ENV, "CI", nothing) == "true", - mathengine = Documenter.MathJax() + mathengine = Documenter.MathJax2(), + collapselevel = 1, ), # See https://github.com/jump-dev/JuMP.jl/issues/1576 strict = true, pages = [ "Introduction" => "index.md", - "Manual" => "apimanual.md", - "Reference" => "apireference.md", - "Testing" => "testing.md", + "Manual" => [ + "manual/basic_usage.md", + "manual/advanced_usage.md", + "manual/implementing.md", + "manual/Benchmarks.md", + "manual/Bridges.md", + "manual/FileFormats.md", + "manual/Test.md", + "manual/Utilities.md", + ], + "API Reference" => "apireference.md", ] ) diff --git a/docs/src/apimanual.md b/docs/src/apimanual.md deleted file mode 100644 index f77a5f411d..0000000000 --- a/docs/src/apimanual.md +++ /dev/null @@ -1,1426 +0,0 @@ -```@meta -CurrentModule = MathOptInterface -DocTestSetup = quote - using MathOptInterface - const MOI = MathOptInterface -end -DocTestFilters = [r"MathOptInterface|MOI"] -``` - -# Manual - -## Purpose - -Each mathematical optimization solver API has its own concepts and data structures for representing optimization models and obtaining results. -However, it is often desirable to represent an instance of an optimization problem at a higher level so that it is easy to try using different solvers. -MathOptInterface (MOI) is an abstraction layer designed to provide a unified interface to mathematical optimization solvers so that users do not need to understand multiple solver-specific APIs. -MOI can be used directly, or through a higher-level modeling interface like [JuMP](https://github.com/jump-dev/JuMP.jl). - -MOI has been designed to replace [MathProgBase](https://github.com/JuliaOpt/MathProgBase.jl), which has been used by modeling packages such as [JuMP](https://github.com/jump-dev/JuMP.jl) and [Convex.jl](https://github.com/jump-dev/Convex.jl). -This second-generation abstraction layer addresses a number of limitations of MathProgBase. -MOI is designed to: -- Be simple and extensible, unifying linear, quadratic, and conic optimization, and seamlessly facilitate extensions to essentially arbitrary constraints and functions (e.g., indicator constraints, complementarity constraints, and piecewise linear functions) -- Be fast by allowing access to a solver's in-memory representation of a problem without writing intermediate files (when possible) and by using multiple dispatch and avoiding requiring containers of nonconcrete types -- Allow a solver to return multiple results (e.g., a pool of solutions) -- Allow a solver to return extra arbitrary information via attributes (e.g., variable- and constraint-wise membership in an irreducible inconsistent subset for infeasibility analysis) -- Provide a greatly expanded set of status codes explaining what happened during the optimization procedure -- Enable a solver to more precisely specify which problem classes it supports -- Enable both primal and dual warm starts -- Enable adding and removing both variables and constraints by indices that are not required to be consecutive -- Enable any modification that the solver supports to an existing model -- Avoid requiring the solver wrapper to store an additional copy of the problem data - -This manual introduces the concepts needed to understand MOI and give a high-level picture of how all of the pieces fit together. The primary focus is on MOI from the perspective of a user of the interface. At the end of the manual we have a section on [Implementing a solver interface](@ref). -The [API Reference](@ref) page lists the complete API. - -MOI does not export functions, but for brevity we often omit qualifying names with the MOI module. Best practice is to have -```julia -using MathOptInterface -const MOI = MathOptInterface -``` -and prefix all MOI methods with `MOI.` in user code. If a name is also available in base Julia, we always explicitly use the module prefix, for example, with `MOI.get`. - -## Standard form problem - -The standard form problem is: - -```math -\begin{align} - & \min_{x \in \mathbb{R}^n} & f_0(x) - \\ - & \;\;\text{s.t.} & f_i(x) & \in \mathcal{S}_i & i = 1 \ldots m -\end{align} -``` - -where: -* the functions ``f_0, f_1, \ldots, f_m`` are specified by - [`AbstractFunction`](@ref) objects -* the sets ``\mathcal{S}_1, \ldots, \mathcal{S}_m`` are specified by - [`AbstractSet`](@ref) objects - -The current function types are: -* **[`SingleVariable`](@ref)**: ``x_j``, i.e., projection onto a single - coordinate defined by a variable index ``j`` -* **[`VectorOfVariables`](@ref)**: projection onto multiple coordinates (i.e., - extracting a subvector) -* **[`ScalarAffineFunction`](@ref)**: ``a^T x + b``, where ``a`` is a vector and - ``b`` scalar -* **[`VectorAffineFunction`](@ref)**: ``A x + b``, where ``A`` is a matrix and - ``b`` is a vector -* **[`ScalarQuadraticFunction`](@ref)**: ``\frac{1}{2} x^T Q x + a^T x + b``, - where ``Q`` is a symmetric matrix, ``a`` is a vector, and ``b`` is a constant -* **[`VectorQuadraticFunction`](@ref)**: a vector of scalar-valued quadratic - functions - -Extensions for nonlinear programming are present but not yet well documented. - -MOI defines some commonly used sets, but the interface is extensible to other -sets recognized by the solver. - -* **[`LessThan(upper)`](@ref MathOptInterface.LessThan)**: - ``\{ x \in \mathbb{R} : x \le \mbox{upper} \}`` -* **[`GreaterThan(lower)`](@ref MathOptInterface.GreaterThan)**: - ``\{ x \in \mathbb{R} : x \ge \mbox{lower} \}`` -* **[`EqualTo(value)`](@ref MathOptInterface.GreaterThan)**: - ``\{ x \in \mathbb{R} : x = \mbox{value} \}`` -* **[`Interval(lower, upper)`](@ref MathOptInterface.Interval)**: - ``\{ x \in \mathbb{R} : x \in [\mbox{lower},\mbox{upper}] \}`` -* **[`Reals(dimension)`](@ref MathOptInterface.Reals)**: - ``\mathbb{R}^\mbox{dimension}`` -* **[`Zeros(dimension)`](@ref MathOptInterface.Zeros)**: ``0^\mbox{dimension}`` -* **[`Nonnegatives(dimension)`](@ref MathOptInterface.Nonnegatives)**: - ``\{ x \in \mathbb{R}^\mbox{dimension} : x \ge 0 \}`` -* **[`Nonpositives(dimension)`](@ref MathOptInterface.Nonpositives)**: - ``\{ x \in \mathbb{R}^\mbox{dimension} : x \le 0 \}`` -* **[`NormInfinityCone(dimension)`](@ref MathOptInterface.NormInfinityCone)**: - ``\{ (t,x) \in \mathbb{R}^\mbox{dimension} : t \ge \lVert x \rVert_\infty = \max_i \lvert x_i \rvert \}`` -* **[`NormOneCone(dimension)`](@ref MathOptInterface.NormOneCone)**: - ``\{ (t,x) \in \mathbb{R}^\mbox{dimension} : t \ge \lVert x \rVert_1 = \sum_i \lvert x_i \rvert \}`` -* **[`SecondOrderCone(dimension)`](@ref MathOptInterface.SecondOrderCone)**: - ``\{ (t,x) \in \mathbb{R}^\mbox{dimension} : t \ge \lVert x \rVert_2 \}`` -* **[`RotatedSecondOrderCone(dimension)`](@ref MathOptInterface.RotatedSecondOrderCone)**: - ``\{ (t,u,x) \in \mathbb{R}^\mbox{dimension} : 2tu \ge \lVert x \rVert_2^2, t,u \ge 0 \}`` -* **[`GeometricMeanCone(dimension)`](@ref MathOptInterface.GeometricMeanCone)**: - ``\{ (t,x) \in \mathbb{R}^{n+1} : x \ge 0, t \le \sqrt[n]{x_1 x_2 \cdots x_n} \}`` - where ``n`` is ``dimension - 1`` -* **[`ExponentialCone()`](@ref MathOptInterface.ExponentialCone)**: - ``\{ (x,y,z) \in \mathbb{R}^3 : y \exp (x/y) \le z, y > 0 \}`` -* **[`DualExponentialCone()`](@ref MathOptInterface.DualExponentialCone)**: - ``\{ (u,v,w) \in \mathbb{R}^3 : -u \exp (v/u) \le exp(1) w, u < 0 \}`` -* **[`PowerCone(exponent)`](@ref MathOptInterface.PowerCone)**: - ``\{ (x,y,z) \in \mathbb{R}^3 : x^\mbox{exponent} y^{1-\mbox{exponent}} \ge |z|, x,y \ge 0 \}`` -* **[`DualPowerCone(exponent)`](@ref MathOptInterface.DualPowerCone)**: - ``\{ (u,v,w) \in \mathbb{R}^3 : \frac{u}{\mbox{exponent}}^\mbox{exponent} - \frac{v}{1-\mbox{exponent}}^{1-\mbox{exponent}} \ge |w|, u,v \ge 0 \}`` -* **[`RelativeEntropyCone(dimension)`](@ref MathOptInterface.RelativeEntropyCone)**: - ``\{ (u, v, w) \in \mathbb{R}^\mbox{dimension} : u \ge \sum_i w_i \log (\frac{w_i}{v_i}), v_i \ge 0, w_i \ge 0 \}`` -* **[`NormSpectralCone(row_dim, column_dim)`](@ref MathOptInterface.NormSpectralCone)**: - ``\{ (t, X) \in \mathbb{R}^{1 + \mbox{row_dim} \times \mbox{column_dim}} : t \ge \sigma_1(X), X \mbox{is a matrix with row_dim rows and column_dim columns} \}`` -* **[`NormNuclearCone(row_dim, column_dim)`](@ref MathOptInterface.NormNuclearCone)**: - ``\{ (t, X) \in \mathbb{R}^{1 + \mbox{row_dim} \times \mbox{column_dim}} : t \ge \sum_i \sigma_i(X), X \mbox{is a matrix with row_dim rows and column_dim columns} \}`` -* **[`PositiveSemidefiniteConeTriangle(dimension)`](@ref MathOptInterface.PositiveSemidefiniteConeTriangle)**: - ``\{ X \in \mathbb{R}^{\mbox{dimension}(\mbox{dimension}+1)/2} : X \mbox{is - the upper triangle of a PSD matrix} \}`` -* **[`PositiveSemidefiniteConeSquare(dimension)`](@ref MathOptInterface.PositiveSemidefiniteConeSquare)**: - ``\{ X \in \mathbb{R}^{\mbox{dimension}^2} : X \mbox{is a PSD matrix} \}`` -* **[`LogDetConeTriangle(dimension)`](@ref MathOptInterface.LogDetConeTriangle)**: - ``\{ (t,u,X) \in \mathbb{R}^{2+\mbox{dimension}(1+\mbox{dimension})/2} : t \le - u\log(\det(X/u)), X \mbox{is the upper triangle of a PSD matrix}, u > 0 \}`` -* **[`LogDetConeSquare(dimension)`](@ref MathOptInterface.LogDetConeSquare)**: - ``\{ (t,u,X) \in \mathbb{R}^{2+\mbox{dimension}^2} : t \le u \log(\det(X/u)), - X \mbox{is a PSD matrix}, u > 0 \}`` -* **[`RootDetConeTriangle(dimension)`](@ref MathOptInterface.RootDetConeTriangle)**: - ``\{ (t,X) \in \mathbb{R}^{1+\mbox{dimension}(1+\mbox{dimension})/2} : t \le - det(X)^{1/\mbox{dimension}}, X \mbox{is the upper triangle of a PSD matrix} \}`` -* **[`RootDetConeSquare(dimension)`](@ref MathOptInterface.RootDetConeSquare)**: - ``\{ (t,X) \in \mathbb{R}^{1+\mbox{dimension}^2} : t \le - \det(X)^{1/\mbox{dimension}}, X \mbox{is a PSD matrix} \}`` -* **[`Integer()`](@ref MathOptInterface.Integer)**: ``\mathbb{Z}`` -* **[`ZeroOne()`](@ref MathOptInterface.ZeroOne)**: ``\{ 0, 1 \}`` -* **[`Semicontinuous(lower,upper)`](@ref MathOptInterface.Semicontinuous)**: - ``\{ 0\} \cup [lower,upper]`` -* **[`Semiinteger(lower,upper)`](@ref MathOptInterface.Semiinteger)**: - ``\{ 0\} \cup \{lower,lower+1,\ldots,upper-1,upper\}`` - - -## The `ModelLike` and `AbstractOptimizer` APIs - -The most significant part of MOI is the definition of the **model API** that is -used to specify an instance of an optimization problem (e.g., by adding -variables and constraints). Objects that implement the model API should inherit -from the [`ModelLike`](@ref) abstract type. - -Notably missing from the model API is the method to solve an optimization problem. -`ModelLike` objects may store an instance (e.g., in memory or backed by a file format) -without being linked to a particular solver. In addition to the model API, MOI -defines [`AbstractOptimizer`](@ref). *Optimizers* (or solvers) implement the -model API (inheriting from `ModelLike`) and additionally provide methods to -solve the model. - -Through the rest of the manual, `model` is used as a generic `ModelLike`, and -`optimizer` is used as a generic `AbstractOptimizer`. - -Models are constructed by -* adding variables using [`add_variable`](@ref) (or [`add_variables`](@ref)), - see [Adding variables](@ref); -* setting an objective sense and function using [`set`](@ref), - see [Setting an objective](@ref); -* and adding constraints using [`add_constraint`](@ref) (or - [`add_constraints`](@ref)), see [Sets and Constraints](@ref). - -The way the problem is solved by the optimimizer is controlled by -[`AbstractOptimizerAttribute`](@ref)s, see [Solver-specific attributes](@ref). - -## Adding variables - -All variables in MOI are scalar variables. -New scalar variables are created with [`add_variable`](@ref) or -[`add_variables`](@ref), which return a [`VariableIndex`](@ref) or -`Vector{VariableIndex}` respectively. `VariableIndex` objects are type-safe -wrappers around integers that refer to a variable in a particular model. - -One uses `VariableIndex` objects to set and get variable attributes. For -example, the [`VariablePrimalStart`](@ref) attribute is used to provide an -initial starting point for a variable or collection of variables: -```julia -v = add_variable(model) -set(model, VariablePrimalStart(), v, 10.5) -v2 = add_variables(model, 3) -set(model, VariablePrimalStart(), v2, [1.3,6.8,-4.6]) -``` - -A variable can be deleted from a model with -[`delete(::ModelLike, ::VariableIndex)`](@ref MathOptInterface.delete(::MathOptInterface.ModelLike, ::MathOptInterface.Index)). -Not all models support deleting variables; an [`DeleteNotAllowed`](@ref) -error is thrown if this is not supported. - -## Functions - -MOI defines six functions as listed in the definition of the -[Standard form problem](@ref). The simplest function is [`SingleVariable`](@ref) -defined as: -```julia -struct SingleVariable <: AbstractFunction - variable::VariableIndex -end -``` - -If `v` is a `VariableIndex` object, then `SingleVariable(v)` is simply the scalar-valued function from the complete set of variables in a model that returns the value of variable `v`. One may also call this function a coordinate projection, which is more useful for defining constraints than as an objective function. - - -A more interesting function is [`ScalarAffineFunction`](@ref), defined as -```julia -struct ScalarAffineFunction{T} <: AbstractScalarFunction - terms::Vector{ScalarAffineTerm{T}} - constant::T -end -``` - -The [`ScalarAffineTerm`](@ref) struct defines -a variable-coefficient pair: -```julia -struct ScalarAffineTerm{T} - coefficient::T - variable_index::VariableIndex -end -``` - -If `x` is a vector of `VariableIndex` objects, then -`ScalarAffineFunction(ScalarAffineTerm.([5.0,-2.3],[x[1],x[2]]),1.0)` represents -the function ``5x_1 - 2.3x_2 + 1``. - -!!! note - - `ScalarAffineTerm.([5.0,-2.3],[x[1],x[2]])` is a shortcut for - `[ScalarAffineTerm(5.0, x[1]), ScalarAffineTerm(-2.3, x[2])]`. This is - Julia's broadcast syntax and is used quite often. - -### Setting an objective - -Objective functions are assigned to a model by setting the -[`ObjectiveFunction`](@ref) attribute. The [`ObjectiveSense`](@ref) attribute is -used for setting the optimization sense. -For example, -```julia -x = add_variables(model, 2) -set(model, ObjectiveFunction{ScalarAffineFunction{Float64}}(), - ScalarAffineFunction(ScalarAffineTerm.([5.0,-2.3],[x[1],x[2]]),1.0)) -set(model, ObjectiveSense(), MIN_SENSE) -``` -sets the objective to the function just discussed in the minimization sense. - -See [Functions and function modifications](@ref) for the complete list of -functions. - -## Sets and Constraints - -All constraints are specified with [`add_constraint`](@ref) by restricting the -output of some function to a set. The interface allows an arbitrary combination -of functions and sets, but of course solvers may decide to support only a small -number of combinations. - -For example, linear programming solvers should support, at least, combinations -of affine functions with the [`LessThan`](@ref) and [`GreaterThan`](@ref) sets. -These are simply linear constraints. `SingleVariable` functions combined with -these same sets are used to specify upper and lower bounds on variables. - -The code example below encodes the linear optimization problem: -```math -\begin{align} -& \max_{x \in \mathbb{R}^2} & 3x_1 + 2x_2 & -\\ -& \;\;\text{s.t.} & x_1 + x_2 &\le 5 -\\ -&& x_1 & \ge 0 -\\ -&&x_2 & \ge -1 -\end{align} -``` - -```julia -x = add_variables(model, 2) -set(model, ObjectiveFunction{ScalarAffineFunction{Float64}}(), - ScalarAffineFunction(ScalarAffineTerm.([3.0, 2.0], x), 0.0)) -set(model, ObjectiveSense(), MAX_SENSE) -add_constraint(model, ScalarAffineFunction(ScalarAffineTerm.(1.0, x), 0.0), - LessThan(5.0)) -add_constraint(model, SingleVariable(x[1]), GreaterThan(0.0)) -add_constraint(model, SingleVariable(x[2]), GreaterThan(-1.0)) -``` - -Besides scalar-valued functions in scalar-valued sets, it's also possible to use vector-valued functions and sets. - -The code example below encodes the convex optimization problem: -```math -\begin{align} -& \max_{x,y,z \in \mathbb{R}} & y + z & -\\ -& \;\;\text{s.t.} & 3x &= 2 -\\ -&& x & \ge \lVert (y,z) \rVert_2 -\end{align} -``` - -```julia -x,y,z = add_variables(model, 3) -set(model, ObjectiveFunction{ScalarAffineFunction{Float64}}(), - ScalarAffineFunction(ScalarAffineTerm.(1.0, [y,z]), 0.0)) -set(model, ObjectiveSense(), MAX_SENSE) -vector_terms = [VectorAffineTerm(1, ScalarAffineTerm(3.0, x))] -add_constraint(model, VectorAffineFunction(vector_terms,[-2.0]), Zeros(1)) -add_constraint(model, VectorOfVariables([x,y,z]), SecondOrderCone(3)) -``` - -[Describe `ConstraintIndex` objects.] - -### Constraints by function-set pairs - -Below is a list of common constraint types and how they are represented -as function-set pairs in MOI. In the notation below, ``x`` is a vector of -decision variables, ``x_i`` is a scalar decision variable, ``\alpha, \beta`` are -scalar constants, ``a, b`` are constant vectors, `A` is a constant matrix and -``\mathbb{R}_+`` (resp. ``\mathbb{R}_-``) is the set of nonnegative (resp. -nonpositive) real numbers. - -#### Linear constraints - -| Mathematical Constraint | MOI Function | MOI Set | -|-------------------------------|------------------------------|----------------| -| ``a^Tx \le \beta`` | `ScalarAffineFunction` | `LessThan` | -| ``a^Tx \ge \alpha`` | `ScalarAffineFunction` | `GreaterThan` | -| ``a^Tx = \beta`` | `ScalarAffineFunction` | `EqualTo` | -| ``\alpha \le a^Tx \le \beta`` | `ScalarAffineFunction` | `Interval` | -| ``x_i \le \beta`` | `SingleVariable` | `LessThan` | -| ``x_i \ge \alpha`` | `SingleVariable` | `GreaterThan` | -| ``x_i = \beta`` | `SingleVariable` | `EqualTo` | -| ``\alpha \le x_i \le \beta`` | `SingleVariable` | `Interval` | -| ``Ax + b \in \mathbb{R}_+^n`` | `VectorAffineFunction` | `Nonnegatives` | -| ``Ax + b \in \mathbb{R}_-^n`` | `VectorAffineFunction` | `Nonpositives` | -| ``Ax + b = 0`` | `VectorAffineFunction` | `Zeros` | - -By convention, solvers are not expected to support nonzero constant terms in the -`ScalarAffineFunction`s the first four rows above, because they are redundant -with the parameters of the sets. For example, ``2x + 1 \le 2`` should be encoded -as ``2x \le 1``. - -Constraints with `SingleVariable` in `LessThan`, `GreaterThan`, `EqualTo`, or -`Interval` sets have a natural interpretation as variable bounds. As such, it is -typically not natural to impose multiple lower or upper bounds on the same -variable, and the solver interfaces should throw respectively -[`LowerBoundAlreadySet`](@ref) or [`UpperBoundAlreadySet`](@ref). -Moreover, adding two `SingleVariable` constraints on the same variable with the -same set is impossible because they share the same index as it is the index of -the variable, see [`ConstraintIndex`](@ref). -It is natural, however, to impose upper and lower bounds separately as two -different constraints on a single variable. The difference between imposing -bounds by using a single `Interval` constraint and by using separate `LessThan` -and `GreaterThan` constraints is that the latter will allow the solver to return -separate dual multipliers for the two bounds, while the former will allow the -solver to return only a single dual for the interval constraint. - -#### Conic constraints - - -| Mathematical Constraint | MOI Function | MOI Set | -|---------------------------------------------------------------|------------------------------|------------------------------------| -| ``\lVert Ax + b\rVert_2 \le c^Tx + d`` | `VectorAffineFunction` | `SecondOrderCone` | -| ``y \ge \lVert x \rVert_2`` | `VectorOfVariables` | `SecondOrderCone` | -| ``2yz \ge \lVert x \rVert_2^2, y,z \ge 0`` | `VectorOfVariables` | `RotatedSecondOrderCone` | -| ``(a_1^Tx + b_1,a_2^Tx + b_2,a_3^Tx + b_3) \in \mathcal{E}`` | `VectorAffineFunction` | `ExponentialCone` | -| ``A(x) \in \mathcal{S}_+`` | `VectorAffineFunction` | `PositiveSemidefiniteConeTriangle` | -| ``B(x) \in \mathcal{S}_+`` | `VectorAffineFunction` | `PositiveSemidefiniteConeSquare` | -| ``x \in \mathcal{S}_+`` | `VectorOfVariables` | `PositiveSemidefiniteConeTriangle` | -| ``x \in \mathcal{S}_+`` | `VectorOfVariables` | `PositiveSemidefiniteConeSquare` | - -where ``\mathcal{E}`` is the exponential cone (see [`ExponentialCone`](@ref)), -``\mathcal{S}_+`` is the set of positive semidefinite symmetric matrices, -``A`` is an affine map that outputs symmetric matrices and -``B`` is an affine map that outputs square matrices. - -#### Quadratic constraints - - -| Mathematical Constraint | MOI Function | MOI Set | -|-------------------------------|------------------------------|-------------------------------| -| ``x^TQx + a^Tx + b \ge 0`` | `ScalarQuadraticFunction` | `GreaterThan` | -| ``x^TQx + a^Tx + b \le 0`` | `ScalarQuadraticFunction` | `LessThan` | -| ``x^TQx + a^Tx + b = 0`` | `ScalarQuadraticFunction` | `EqualTo` | -| Bilinear matrix inequality | `VectorQuadraticFunction` | `PositiveSemidefiniteCone...` | - - -#### Discrete and logical constraints - - -| Mathematical Constraint | MOI Function | MOI Set | -|--------------------------------------------------------------------------------------------|------------------------|------------------------------------| -| ``x_i \in \mathbb{Z}`` | `SingleVariable` | `Integer` | -| ``x_i \in \{0,1\}`` | `SingleVariable` | `ZeroOne` | -| ``x_i \in \{0\} \cup [l,u]`` | `SingleVariable` | `Semicontinuous` | -| ``x_i \in \{0\} \cup \{l,l+1,\ldots,u-1,u\}`` | `SingleVariable` | `Semiinteger` | -| At most one component of ``x`` can be nonzero | `VectorOfVariables` | `SOS1` | -| At most two components of ``x`` can be nonzero, and if so they must be adjacent components | `VectorOfVariables` | `SOS2` | -| ``y = 1 \implies a^T x \in S`` | `VectorAffineFunction` | `IndicatorSet` | - -## Solving and retrieving the results - -Once an optimizer is loaded with the objective function and all of the -constraints, we can ask the solver to solve the model by calling -[`optimize!`](@ref). -```julia -optimize!(optimizer) -``` - -The optimization procedure may terminate for a number of reasons. The -[`TerminationStatus`](@ref) attribute of the optimizer returns a -[`TerminationStatusCode`](@ref) object which explains why the solver stopped. -The termination statuses distinguish between proofs of optimality, -infeasibility, local convergence, limits, and termination because of something -unexpected like invalid problem data or failure to converge. A typical usage of -the `TerminationStatus` attribute is as follows: -```julia -status = MOI.get(optimizer, TerminationStatus()) -if status == MOI.OPTIMAL - # Ok, we solved the problem! -else - # Handle other cases. -end -``` - -After checking the `TerminationStatus`, one should typically check -[`ResultCount`](@ref). This attribute returns the number of results that the -solver has available to return. *A result is defined as a primal-dual pair, -but either the primal or the dual may be missing from the result.* While the -`OPTIMAL` termination status normally implies that at least one result is -available, other statuses do not. For example, in the case of infeasiblity, -a solver may return no result or a proof of infeasibility. The `ResultCount` -distinguishes between these two cases. - -The [`PrimalStatus`](@ref) and [`DualStatus`](@ref) attributes return a -[`ResultStatusCode`](@ref) that indicates if that component of the result -is present (i.e., not `NO_SOLUTION`) and explains how to interpret the result. - -If `PrimalStatus` is not `NO_SOLUTION`, then the primal may be retrieved with the -[`VariablePrimal`](@ref) attribute: -```julia -MOI.get(optimizer, VariablePrimal(), x) -``` -If `x` is a `VariableIndex` then the function call returns a scalar, and if `x` is a `Vector{VariableIndex}` then the call returns a vector of scalars. `VariablePrimal()` is equivalent to `VariablePrimal(1)`, i.e., the variable primal vector of the first result. Use `VariablePrimal(N)` to access the `N`th result. - -See also the attributes [`ConstraintPrimal`](@ref), and -[`ConstraintDual`](@ref). -See [Duals](@ref) for a discussion of the MOI conventions for primal-dual pairs -and certificates. - -!!! note - We omit discussion of how to handle multiple results, i.e., when - `ResultCount` is greater than 1. This is supported in the API but not yet - implemented in any solver. - -### Common status situations - -The sections below describe how to interpret typical or interesting status cases -for three common classes of solvers. The example cases are illustrative, not -comprehensive. Solver wrappers may provide additional information on -how the solver's statuses map to MOI statuses. - -`?` in the tables indicate that multiple different values are possible. - -#### Primal-dual convex solver - -Linear programming and conic optimization solvers fall into this category. - -| What happened? | `TerminationStatus()` | `ResultCount()` | `PrimalStatus()` | `DualStatus()` | -| --------------------------------------- | --------------------- | --------------- | ------------------------------------------- | ------------------------------------------- | -| Proved optimality | `OPTIMAL` | 1 | `FEASIBLE_POINT` | `FEASIBLE_POINT` | -| Proved infeasible | `INFEASIBLE` | 1 | `NO_SOLUTION` | `INFEASIBILITY_CERTIFICATE` | -| Optimal within relaxed tolerances | `ALMOST_OPTIMAL` | 1 | `FEASIBLE_POINT` or `ALMOST_FEASIBLE_POINT` | `FEASIBLE_POINT` or `ALMOST_FEASIBLE_POINT` | -| Detected an unbounded ray of the primal | `DUAL_INFEASIBLE` | 1 | `INFEASIBILITY_CERTIFICATE` | `NO_SOLUTION` | -| Stall | `SLOW_PROGRESS` | 1 | ? | ? | - -#### Global branch-and-bound solvers - -Mixed-integer programming solvers fall into this category. - -| What happened? | `TerminationStatus()` | `ResultCount()` | `PrimalStatus()` | `DualStatus()` | -| ------------------------------------------------ | ------------------------- | --------------- | ------------------ | -------------- | -| Proved optimality | `OPTIMAL` | 1 | `FEASIBLE_POINT` | `NO_SOLUTION` | -| Presolve detected infeasibility or unboundedness | `INFEASIBLE_OR_UNBOUNDED` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | -| Proved infeasibility | `INFEASIBLE` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | -| Timed out (no solution) | `TIME_LIMIT` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | -| Timed out (with a solution) | `TIME_LIMIT` | 1 | `FEASIBLE_POINT` | `NO_SOLUTION` | -| `CPXMIP_OPTIMAL_INFEAS` | `ALMOST_OPTIMAL` | 1 | `INFEASIBLE_POINT` | `NO_SOLUTION` | - -[`CPXMIP_OPTIMAL_INFEAS`](https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.6.1/ilog.odms.cplex.help/refcallablelibrary/macros/CPXMIP_OPTIMAL_INFEAS.html) -is a CPLEX status that indicates that a preprocessed problem was solved to -optimality, but the solver was unable to recover a feasible solution to the -original problem. - -#### Local search solvers - -Nonlinear programming solvers fall into this category. It also includes -non-global tree search solvers like -[Juniper](https://github.com/lanl-ansi/Juniper.jl). - -| What happened? | `TerminationStatus()` | `ResultCount()` | `PrimalStatus()` | `DualStatus()` | -| ------------------------------------------------------ | --------------------------------- | --------------- | ------------------ | ---------------- | -| Converged to a stationary point | `LOCALLY_SOLVED` | 1 | `FEASIBLE_POINT` | `FEASIBLE_POINT` | -| Completed a non-global tree search (with a solution) | `LOCALLY_SOLVED` | 1 | `FEASIBLE_POINT` | `FEASIBLE_POINT` | -| Converged to an infeasible point | `LOCALLY_INFEASIBLE` | 1 | `INFEASIBLE_POINT` | ? | -| Completed a non-global tree search (no solution found) | `LOCALLY_INFEASIBLE` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | -| Iteration limit | `ITERATION_LIMIT` | 1 | ? | ? | -| Diverging iterates | `NORM_LIMIT` or `OBJECTIVE_LIMIT` | 1 | ? | ? | - - -## A complete example: solving a knapsack problem - -We first need to select a solver supporting the given problem (see -[`supports`](@ref) and [`supports_constraint`](@ref)). In this example, we -want to solve a binary-constrained knapsack problem: -`max c'x: w'x <= C, x binary`. Suppose we choose GLPK: -```julia -using GLPK -optimizer = GLPK.Optimizer() -``` -We first define the constants of the problem: -```jldoctest knapsack; setup = :(optimizer = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()); MOI.Utilities.set_mock_optimize!(optimizer, mock -> MOI.Utilities.mock_optimize!(mock, ones(3)))) -c = [1.0, 2.0, 3.0] -w = [0.3, 0.5, 1.0] -C = 3.2 - -# output - -3.2 -``` -We create the variables of the problem and set the objective function: -```jldoctest knapsack -x = MOI.add_variables(optimizer, length(c)) -objective_function = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(c, x), 0.0) -MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - objective_function) -MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) - -# output - -MAX_SENSE::OptimizationSense = 1 -``` -We add the knapsack constraint and integrality constraints: -```jldoctest knapsack -knapsack_function = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0) -MOI.add_constraint(optimizer, knapsack_function, MOI.LessThan(C)) -for x_i in x - MOI.add_constraint(optimizer, MOI.SingleVariable(x_i), MOI.ZeroOne()) -end - -# output - -``` -We are all set! We can now call [`optimize!`](@ref) and wait for the solver to -find the solution: -```jldoctest knapsack -MOI.optimize!(optimizer) - -# output - -``` -The first thing to check after optimization is why the solver stopped, e.g., -did it stop because of a time limit or did it stop because it found the optimal -solution? -```jldoctest knapsack -MOI.get(optimizer, MOI.TerminationStatus()) - -# output - - -OPTIMAL::TerminationStatusCode = 1 -``` -It found the optimal solution! Now let's see what is that solution. -```jldoctest knapsack -MOI.get(optimizer, MOI.PrimalStatus()) - -# output - -FEASIBLE_POINT::ResultStatusCode = 1 -``` -What is its objective value? -```jldoctest knapsack -MOI.get(optimizer, MOI.ObjectiveValue()) - -# output - -6.0 -``` -And what is the value of the variables `x`? -```jldoctest knapsack -MOI.get(optimizer, MOI.VariablePrimal(), x) - -# output - -3-element Array{Float64,1}: - 1.0 - 1.0 - 1.0 -``` - -## Problem modification - -In addition to adding and deleting constraints and variables, MathOptInterface -supports modifying, in-place, coefficients in the constraints and the objective -function of a model. These modifications can be grouped into two categories: -modifications which replace the set of function of a constraint with a new set -or function; and modifications which change, in-place, a component of a -function. - -In the following, we detail the various ways this can be -achieved. Readers should note that some solvers will not support problem -modification. - -### Replacements - -First, we discuss how to replace the set or function of a constraint with a new -instance of the same type. - -#### The set of a constraint - -Given a constraint of type `F`-in-`S` (see [Constraints by function-set pairs](@ref) - above for an explanation), we can modify parameters (but not the type) of the - set `S` by replacing it with a new instance of the same type. For example, - given the variable bound ``x \le 1``: -```julia -c = add_constraint(m, SingleVariable(x), LessThan(1.0)) -``` -we can modify the set so that the bound now ``x \le 2`` as follows: -```julia -set(m, ConstraintSet(), c, LessThan(2.0)) -``` -where `m` is our [`ModelLike`](@ref) model. However, the following will fail as -the new set (`GreaterThan`) is of a different type to the original set -(`LessThan`): -```julia -set(m, ConstraintSet(), c, GreaterThan(2.0)) # errors -``` -If our constraint is an affine inequality, then this corresponds to modifying -the right-hand side of a constraint in linear programming. - -In some special cases, solvers may support efficiently changing the set of a -constraint (for example, from [`LessThan`](@ref) to [`GreaterThan`](@ref)). For -these cases, MathOptInterface provides the [`transform`](@ref) method. For -example, instead of the error we observed above, the following will -work: -```julia -c2 = transform(m, c, GreaterThan(1.0)) -``` -The [`transform`](@ref) function returns a new constraint index, and the old -constraint index (i.e., `c`) is no longer valid: -```julia -is_valid(m, c) # false -is_valid(m, c2) # true -``` -Also note that [`transform`](@ref) cannot be called with a set of the same type; -[`set`](@ref) should be used instead. - -#### The function of a constraint - -Given a constraint of type `F`-in-`S` (see [Constraints by function-set pairs](@ref) -above for an explanation), it is also possible to modify the function of type -`F` by replacing it with a new instance of the same type. For example, given the -variable bound ``x \le 1``: -```julia -c = add_constraint(m, SingleVariable(x), LessThan(1.0)) -``` -we can modify the function so that the bound now ``y \le 1`` as follows: -```julia -set(m, ConstraintFunction(), c, SingleVariable(y)) -``` -where `m` is our [`ModelLike`](@ref) model. However, the following will fail as -the new function is of a different type to the original function: -```julia -set(m, ConstraintFunction(), c, - ScalarAffineFunction([ScalarAffineTerm(1.0, x)], 0.0) -) -``` - -### In-place modification - -The second type of problem modifications allow the user to modify, in-place, the -coefficients of a function. Currently, four modifications are supported by -MathOptInterface. They are: - 1. change the constant term in a scalar function; - 2. change the constant term in a vector function; - 3. change the affine coefficients in a scalar function; and - 4. change the affine coefficients in a vector function. - -To distinguish between the replacement of the function with a new instance -(described above) and the modification of an existing function, the in-place -modifications use the [`modify`](@ref) method: -```julia -modify(model, index, change::AbstractFunctionModification) -``` -[`modify`](@ref) takes three arguments. The first is the [`ModelLike`](@ref) -model `model`, the second is the constraint index, and the third is an instance -of an [`AbstractFunctionModification`](@ref). - -We now detail each of these four in-place modifications. - -#### Constant term in a scalar function - -MathOptInterface supports is the ability to modify the constant term within a -[`ScalarAffineFunction`](@ref) and a [`ScalarQuadraticFunction`](@ref) using -the [`ScalarConstantChange`](@ref) subtype of -[`AbstractFunctionModification`](@ref). This includes the objective function, as -well as the function in a function-pair constraint. - -For example, consider a problem `m` with the objective ``\max 1.0x + 0.0``: -```julia -set(m, - ObjectiveFunction{ScalarAffineFunction{Float64}}(), - ScalarAffineFunction([ScalarAffineTerm(1.0, x)], 0.0) -) -``` -We can modify the constant term in the objective function as follows: -```julia -modify(m, - ObjectiveFunction{ScalarAffineFunction{Float64}}(), - ScalarConstantChange(1.0) -) -``` -The objective function will now be ``\max 1.0x + 1.0``. - -#### Constant terms in a vector function - -We can modify the constant terms in a [`VectorAffineFunction`](@ref) or a -[`VectorQuadraticFunction`](@ref) using the [`VectorConstantChange`](@ref) -subtype of [`AbstractFunctionModification`](@ref). - -For example, consider a model with the following -`VectorAffineFunction`-in-`Nonpositives` constraint: -```julia -c = add_constraint(m, - VectorAffineFunction([ - VectorAffineTerm(1, ScalarAffineTerm(1.0, x)), - VectorAffineTerm(1, ScalarAffineTerm(2.0, y)) - ], - [0.0, 0.0] - ), - Nonpositives(2) -) -``` -We can modify the constant vector in the [`VectorAffineFunction`](@ref) from -`[0.0, 0.0]` to `[1.0, 2.0]` as follows: -```julia -modify(m, c, VectorConstantChange([1.0, 2.0]) -) -``` -The constraints are now ``1.0x + 1.0 \le 0.0`` and ``2.0y + 2.0 \le 0.0``. - -#### Affine coefficients in a scalar function - -In addition to modifying the constant terms in a function, we can also modify -the affine variable coefficients in an [`ScalarAffineFunction`](@ref) or a -[`ScalarQuadraticFunction`](@ref) using the [`ScalarCoefficientChange`](@ref) -subtype of [`AbstractFunctionModification`](@ref). - -For example, given the constraint ``1.0x <= 1.0``: -```julia -c = add_constraint(m, - ScalarAffineFunction([ScalarAffineTerm(1.0, x)], 0.0), - LessThan(1.0) -) -``` -we can modify the coefficient of the `x` variable so that the constraint becomes -``2.0x <= 1.0`` as follows: -```julia -modify(m, c, ScalarCoefficientChange(x, 2.0)) -``` - -[`ScalarCoefficientChange`](@ref) can also be used to modify the objective -function by passing an instance of [`ObjectiveFunction`](@ref) instead of the -constraint index `c` as we saw above. - -#### Affine coefficients in a vector function - -Finally, the last modification supported by MathOptInterface is the ability to -modify the affine coefficients of a single variable in a -[`VectorAffineFunction`](@ref) or a [`VectorQuadraticFunction`](@ref) using -the [`MultirowChange`](@ref) subtype of [`AbstractFunctionModification`](@ref). - -For example, given the constraint ``Ax \in \mathbb{R}^2_+``, where -``A = [1.0, 2.0]^\top``: -```julia -c = add_constraint(m, - VectorAffineFunction([ - VectorAffineTerm(1, ScalarAffineTerm(1.0, x)), - VectorAffineTerm(1, ScalarAffineTerm(2.0, x)) - ], - [0.0, 0.0] - ), - Nonnegatives(2) -) -``` -we can modify the coefficients of the `x` variable so that the `A` matrix -becomes ``A = [3.0, 4.0]^\top`` as follows: -```julia -modify(m, c, MultirowChange(x, [3.0, 4.0])) -``` - -## File formats - -The `FileFormats` module provides functionality for reading and writing MOI models using -[`write_to_file`](@ref) and [`read_from_file`](@ref). - -To write a model `src` to a MathOptFormat file, use: -```julia -src = # ... -dest = FileFormats.Model(format = FileFormats.FORMAT_MOF) -MOI.copy_to(dest, src) -MOI.write_to_file(dest, "file.mof.json") -``` -The list of supported formats is given by the [`FileFormats.FileFormat`](@ref) enum. - -Instead of the `format` keyword, you can also use the `filename` keyword argument to -[`FileFormats.Model`](@ref). This will attempt to automatically guess the format from the -file extension. For example: -```julia -src = # ... -filename = "my_model.cbf.gz" -dest = FileFormats.Model(filename = filename) -MOI.copy_to(dest, src) -MOI.write_to_file(dest, filename) - -src_2 = FileFormats.Model(filename = filename) -MOI.read_from_file(src_2, filename) -``` -Note how the compression format (GZip) is also automatically detected from the filename. - -In some cases `src` may contain constraints that are not supported by the file format (e.g., -the CBF format supports integer variables but not binary). If so, you should copy `src` to a -bridged model using [`Bridges.full_bridge_optimizer`](@ref): -```julia -src = # ... conic model ... -dest = FileFormats.Model(format = FileFormats.FORMAT_CBF) -bridged = MOI.Bridges.full_bridge_optimizer(dest, Float64) -MOI.copy_to(bridged, src) -MOI.write_to_file(dest, "my_model.cbf") -``` -You should also note that even after bridging, it may still not be possible to write the -model to file because of unsupported constraints (e.g., PSD variables in the LP file -format). - -In addition to [`write_to_file`](@ref) and [`read_from_file`](@ref), you can read and write -directly from `IO` streams using `Base.write` and `Base.read!`: -```julia -src = # ... -io = IOBuffer() -dest = FileFormats.Model(format = FileFormats.FORMAT_MPS) -MOI.copy_to(dest, src) -write(io, dest) - -seekstart(io) -src_2 = FileFormats.Model(format = FileFormats.FORMAT_MPS) -read!(io, src_2) -``` - -## Advanced - -### Duals - -Conic duality is the starting point for MOI's duality conventions. When all functions are affine (or coordinate projections), and all constraint sets are closed convex cones, the model may be called a conic optimization problem. -For a minimization problem in geometric conic form, the primal is: - -```math -\begin{align} -& \min_{x \in \mathbb{R}^n} & a_0^T x + b_0 -\\ -& \;\;\text{s.t.} & A_i x + b_i & \in \mathcal{C}_i & i = 1 \ldots m -\end{align} -``` - -and the dual is a maximization problem in standard conic form: - -```math -\begin{align} -& \max_{y_1, \ldots, y_m} & -\sum_{i=1}^m b_i^T y_i + b_0 -\\ -& \;\;\text{s.t.} & a_0 - \sum_{i=1}^m A_i^T y_i & = 0 -\\ -& & y_i & \in \mathcal{C}_i^* & i = 1 \ldots m -\end{align} -``` - -where each ``\mathcal{C}_i`` is a closed convex cone and ``\mathcal{C}_i^*`` is its dual cone. - -For a maximization problem in geometric conic form, the primal is: -```math -\begin{align} -& \max_{x \in \mathbb{R}^n} & a_0^T x + b_0 -\\ -& \;\;\text{s.t.} & A_i x + b_i & \in \mathcal{C}_i & i = 1 \ldots m -\end{align} -``` - -and the dual is a minimization problem in standard conic form: - -```math -\begin{align} -& \min_{y_1, \ldots, y_m} & \sum_{i=1}^m b_i^T y_i + b_0 -\\ -& \;\;\text{s.t.} & a_0 + \sum_{i=1}^m A_i^T y_i & = 0 -\\ -& & y_i & \in \mathcal{C}_i^* & i = 1 \ldots m -\end{align} -``` - - - -A linear inequality constraint ``a^T x + b \ge c`` should be interpreted as ``a^T x + b - c \in \mathbb{R}_+``, and similarly ``a^T x + b \le c`` should be interpreted as ``a^T x + b - c \in \mathbb{R}_-``. -Variable-wise constraints should be interpreted as affine constraints with the appropriate identity mapping in place of ``A_i``. - -For the special case of minimization LPs, the MOI primal form can be stated as -```math -\begin{align} -& \min_{x \in \mathbb{R}^n} & a_0^T x &+ b_0 -\\ -& \;\;\text{s.t.} -&A_1 x & \ge b_1\\ -&& A_2 x & \le b_2\\ -&& A_3 x & = b_3 -\end{align} -``` - -By applying the stated transformations to conic form, taking the dual, and transforming back into linear inequality form, one obtains the following dual: - -```math -\begin{align} -& \max_{y_1,y_2,y_3} & b_1^Ty_1 + b_2^Ty_2 + b_3^Ty_3 &+ b_0 -\\ -& \;\;\text{s.t.} -&A_1^Ty_1 + A_2^Ty_2 + A_3^Ty_3 & = a_0\\ -&& y_1 &\ge 0\\ -&& y_2 &\le 0 -\end{align} -``` - -For maximization LPs, the MOI primal form can be stated as: -```math -\begin{align} -& \max_{x \in \mathbb{R}^n} & a_0^T x &+ b_0 -\\ -& \;\;\text{s.t.} -&A_1 x & \ge b_1\\ -&& A_2 x & \le b_2\\ -&& A_3 x & = b_3 -\end{align} -``` - -and similarly, the dual is: -```math -\begin{align} -& \min_{y_1,y_2,y_3} & -b_1^Ty_1 - b_2^Ty_2 - b_3^Ty_3 &+ b_0 -\\ -& \;\;\text{s.t.} -&A_1^Ty_1 + A_2^Ty_2 + A_3^Ty_3 & = -a_0\\ -&& y_1 &\ge 0\\ -&& y_2 &\le 0 -\end{align} -``` - -An important note for the LP case is that the signs of the feasible duals depend only on the sense of the inequality and not on the objective sense. - -#### Duality and scalar product - -The scalar product is different from the canonical one for the sets -[`PositiveSemidefiniteConeTriangle`](@ref), [`LogDetConeTriangle`](@ref), -[`RootDetConeTriangle`](@ref). -If the set ``C_i`` of the section [Duals](@ref) is one of these three cones, -then the rows of the matrix ``A_i`` corresponding to off-diagonal entries are -twice the value of the `coefficients` field in the `VectorAffineFunction` for -the corresponding rows. See [`PositiveSemidefiniteConeTriangle`](@ref) for -details. - -#### Dual for problems with quadratic functions - -Given a problem with quadratic functions: -```math -\begin{align*} -& \min_{x \in \mathbb{R}^n} & \frac{1}{2}x^TQ_0x + a_0^T x + b_0 -\\ -& \;\;\text{s.t.} & \frac{1}{2}x^TQ_ix + a_i^T x + b_i & \in \mathcal{C}_i & i = 1 \ldots m -\end{align*} -``` -Consider the Lagrangian function -```math -L(x, y) = \frac{1}{2}x^TQ_0x + a_0^T x + b_0 - \sum_{i = 1}^m y_i (\frac{1}{2}x^TQ_ix + a_i^T x + b_i) -``` -A pair of primal-dual variables $(x^\star, y^\star)$ is optimal if -* ``x^\star`` is a minimizer of - ```math - \min_{x \in \mathbb{R}^n} L(x, y^\star). - ``` - That is, - ```math - 0 = \nabla_x L(x, y^\star) = Q_0x + a_0 - \sum_{i = 1}^m y_i^\star (Q_ix + a_i). - ``` -* and ``y^\star`` is a maximizer of - ```math - \max_{y_i \in \mathcal{C}_i^*} L(x^\star, y). - ``` - That is, for all ``i = 1, \ldots, m``, ``\frac{1}{2}x^TQ_ix + a_i^T x + b_i`` is - either zero or in the normal cone of ``\mathcal{C}_i^*`` at ``y^\star``. - For instance, if ``\mathcal{C}_i`` is ``\{ x \in \mathbb{R} : x \le 0 \}``, it means that - if ``\frac{1}{2}x^TQ_ix + a_i^T x + b_i`` is nonzero then ``\lambda_i = 0``, - this is the classical complementary slackness condition. - -If ``\mathcal{C}_i`` is a vector set, the discussion remains valid with -``y_i(\frac{1}{2}x^TQ_ix + a_i^T x + b_i)`` replaced with the scalar product -between ``y_i`` and the vector of scalar-valued quadratic functions. - -!!! note - For quadratic programs with only affine constraints, the optimality condition - ``\nabla_x L(x, y^\star) = 0`` can be simplified as follows - ```math - 0 = \nabla_x L(x, y^\star) = Q_0x + a_0 - \sum_{i = 1}^m y_i^\star a_i - ``` - which gives - ```math - Q_0x = \sum_{i = 1}^m y_i^\star a_i - a_0 - ``` - The Lagrangian function - ```math - L(x, y) = \frac{1}{2}x^TQ_0x + a_0^T x + b_0 - \sum_{i = 1}^m y_i (a_i^T x + b_i) - ``` - can be rewritten as - ```math - L(x, y) = \frac{1}{2}x^TQ_0x - (\sum_{i = 1}^m y_i a_i^T - a_0^T) x + b_0 - \sum_{i = 1}^m y_i (a_i^T x + b_i) - ``` - which, using the optimality condition ``\nabla_x L(x, y^\star) = 0``, can be simplified as - ```math - L(x, y) = -\frac{1}{2}x^TQ_0x + b_0 - \sum_{i = 1}^m y_i (a_i^T x + b_i) - ``` - - -### Automatic reformulation - -#### Variable reformulation - -A variable is often created constrained in a set unsupported by the solver while -it could be parametrized by variables constrained in supported sets. -For example, the [`Bridges.Variable.VectorizeBridge`](@ref) defines the -reformulation of a constrained variable in [`GreaterThan`](@ref) into a -constrained vector of one variable in [`Nonnegatives`](@ref). -The `Bridges.Variable.Vectorize` is the bridge optimizer that applies the -[`Bridges.Variable.VectorizeBridge`](@ref) rewriting rule. Given an optimizer -`optimizer` implementing constrained variables in [`Nonnegatives`](@ref), -the optimizer -```jldoctest; setup=:(optimizer = MOI.Utilities.Model{Float64}()) -bridged_optimizer = MOI.Bridges.Variable.Vectorize{Float64}(optimizer) -MOI.supports_add_constrained_variable(bridged_optimizer, MOI.GreaterThan{Float64}) - -# output - -true -``` -will additionally support constrained variables in [`GreaterThan`](@ref). -Note that these [`Bridges.Variable.SingleBridgeOptimizer`](@ref) are mainly -used for testing bridges. It is recommended to rather use -[`Bridges.full_bridge_optimizer`](@ref), which automatically selects the -appropriate bridges for unsupported constrained variables. - -#### Constraint reformulation - -A constraint can often be written in a number of equivalent formulations. For -example, the constraint ``l \le a^\top x \le u`` -(`ScalarAffineFunction`-in-`Interval`) could be re-formulated as two -constraints: ``a^\top x \ge l`` (`ScalarAffineFunction`-in-`GreaterThan`) and -``a^\top x \le u`` (`ScalarAffineFunction`-in-`LessThan`). An alternative -re-formulation is to add a dummy variable `y` with the constraints ``l \le y \le -u`` (`SingleVariable`-in-`Interval`) and ``a^\top x - y = 0`` -(`ScalarAffineFunction`-in-`EqualTo`). - -To avoid each solver having to code these transformations manually, -MathOptInterface provides *bridges*. A bridge is a small transformation from one -constraint type to another (potentially collection of) constraint type. Because -these bridges are included in MathOptInterface, they can be re-used by any -optimizer. Some bridges also implement constraint modifications and constraint -primal and dual translations. - -For example, the `SplitIntervalBridge` defines the reformulation of a -`ScalarAffineFunction`-in-`Interval` constraint into a -`ScalarAffineFunction`-in-`GreaterThan` and a -`ScalarAffineFunction`-in-`LessThan` constraint. `SplitInterval` is the -bridge optimizer that applies the `SplitIntervalBridge` rewriting rule. Given -an optimizer `optimizer` implementing `ScalarAffineFunction`-in-`GreaterThan` -and `ScalarAffineFunction`-in-`LessThan`, the optimizer -```jldoctest; setup=:(optimizer = MOI.Utilities.Model{Float64}()) -bridged_optimizer = MOI.Bridges.Constraint.SplitInterval{Float64}(optimizer) -MOI.supports_constraint(bridged_optimizer, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) - -# output - -true -``` -will additionally support `ScalarAffineFunction`-in-`Interval`. -Note that these [`Bridges.Constraint.SingleBridgeOptimizer`](@ref)s are mainly -used for testing bridges. It is recommended to rather use -[`Bridges.full_bridge_optimizer`](@ref) which automatically selects the -appropriate constraint bridges for unsupported constraints. - - -## Implementing a solver interface - -[The interface is designed for multiple dispatch, e.g., attributes, combinations of sets and functions.] - -### Solver-specific attributes - -Solver-specific attributes should be specified by creating an -[`AbstractOptimizerAttribute`](@ref). For example, inside `MyPackage`, we could -add the following: -```julia -struct PrintLevel <: MOI.AbstractOptimizerAttribute end -function MOI.set(model::Optimizer, ::PrintLevel, level::Int) - # ... set the print level ... -end -``` -Then, the user can write: -```julia -model = MyPackage.Optimizer() -MOI.set(model, MyPackage.PrintLevel(), 0) -``` - -### Supported constrained variables and constraints - -The solver interface should only implement support for variables -constrained on creation (see -[`add_constrained_variable`](@ref)/[`add_constrained_variables`](@ref)) or -constraints that directly map to a structure exploited by the solver algorithm. -There is no need to add support for additional types, this is handled by the -[Automatic reformulation](@ref). Furthermore, this allows -[`supports_constraint`](@ref) to indicate which types are exploited by the -solver and hence allows layers such as [`Bridges.LazyBridgeOptimizer`](@ref) -to accurately select the most appropriate transformations. - -As [`add_constrained_variable`](@ref) (resp. [`add_constrained_variables`](@ref)) -falls back to [`add_variable`](@ref) (resp. [`add_variables`](@ref)) followed by -[`add_constraint`](@ref), there is no need to implement this function -if `model` does not require that variables be constrained when they are created. -However, if `model` requires that variables be constrained when they're created, -then it should only implement [`add_constrained_variable`](@ref) and not -[`add_variable`](@ref) nor [`add_constraint`](@ref) for -[`SingleVariable`](@ref)-in-`typeof(set)`. In addition, it should implement -`supports_add_constrained_variables(::Optimizer, ::Type{Reals})` and return -`false` so that these variables are bridged, see -[`supports_add_constrained_variables`](@ref). - -### Handling duplicate coefficients - -Solvers should expect that functions such as `ScalarAffineFunction` and -`VectorQuadraticFunction` may contain duplicate coefficents, for example, -`ScalarAffineFunction([ScalarAffineTerm(x, 1), ScalarAffineTerm(x, 1)], 0.0)`. -These duplicate terms can be aggregated by calling -[`Utilities.canonical`](@ref). - -```jldoctest; setup = :(using MathOptInterface) -x = MathOptInterface.VariableIndex(1) -term = MathOptInterface.ScalarAffineTerm(1, x) -func = MathOptInterface.ScalarAffineFunction([term, term], 0) -func_canon = MathOptInterface.Utilities.canonical(func) -func_canon ≈ MathOptInterface.ScalarAffineFunction( - [MathOptInterface.ScalarAffineTerm(2, x)], 0) - -# output - -true -``` - -### Implementing copy - -Avoid storing extra copies of the problem when possible. This means that solver -wrappers should not use [`Utilities.CachingOptimizer`](@ref) as part of the wrapper. -Instead, do one of the following to load the problem (assuming the solver -wrapper type is called `Optimizer`): - -* If the solver supports loading the problem incrementally, implement - [`add_variable`](@ref), [`add_constraint`](@ref) for supported constraints and - [`set`](@ref) for supported attributes and add: - ```julia - function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...) - return MOI.Utilities.automatic_copy_to(dest, src; kws...) - end - ``` - with - ```julia - MOI.Utilities.supports_default_copy_to(model::Optimizer, copy_names::Bool) = true - ``` - or - ```julia - MOI.Utilities.supports_default_copy_to(model::Optimizer, copy_names::Bool) = !copy_names - ``` - depending on whether the solver support names; see - [`Utilities.supports_default_copy_to`](@ref) for more details. -* If the solver does not support loading the problem incrementally, do not - implement [`add_variable`](@ref) and [`add_constraint`](@ref) as implementing - them would require caching the problem. Let users or JuMP decide whether to - use a `CachingOptimizer` instead. Write either a custom implementation of - [`copy_to`](@ref) or implement the [Allocate-Load API](@ref). If you choose to - implement the Allocate-Load API, - do - ```julia - function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...) - return MOI.Utilities.automatic_copy_to(dest, src; kws...) - end - ``` - with - ```julia - MOI.Utilities.supports_allocate_load(model::Optimizer, copy_names::Bool) = true - ``` - or - ```julia - MOI.Utilities.supports_allocate_load(model::Optimizer, copy_names::Bool) = !copy_names - ``` - depending on whether the solver support names; see - [`Utilities.supports_allocate_load`](@ref) for more details. - - Note that even if both writing a custom implementation of [`copy_to`](@ref) - and implementing the [Allocate-Load API](@ref) requires the user to copy the - model from a cache, the [Allocate-Load API](@ref) allows MOI layers to be - added between the cache and the solver which allows transformations to be - applied without the need for additional caching. For instance, with the - proposed [Light bridges](https://github.com/jump-dev/MathOptInterface.jl/issues/523), - no cache will be needed to store the bridged model when bridges are used by - JuMP so implementing the [Allocate-Load API](@ref) will allow JuMP to use only - one cache instead of two. - -### JuMP mapping - -MOI defines a very general interface, with multiple possible ways to describe the same constraint. -This is considered a feature, not a bug. -MOI is designed to make it possible to experiment with alternative representations of an optimization problem at both the solving and modeling level. -When implementing an interface, it is important to keep in mind that the way the user can express problems in JuMP is not directly limited by the constraints which a solver supports via MOI as JuMP performs [Automatic reformulation](@ref). -Therefore, we recommend to only support the constraint types that directly map to a structure exploited by the solver algorithm. -The following bullet points show examples of how JuMP constraints are translated into MOI function-set pairs: - - `@constraint(m, 2x + y <= 10)` becomes `ScalarAffineFunction`-in-`LessThan`; - - `@constraint(m, 2x + y >= 10)` becomes `ScalarAffineFunction`-in-`GreaterThan`; - - `@constraint(m, 2x + y == 10)` becomes `ScalarAffineFunction`-in-`EqualTo`; - - `@constraint(m, 0 <= 2x + y <= 10)` becomes `ScalarAffineFunction`-in-`Interval`; - - `@constraint(m, 2x + y in ArbitrarySet())` becomes - `ScalarAffineFunction`-in-`ArbitrarySet`. - -Variable bounds are handled in a similar fashion: - - `@variable(m, x <= 1)` becomes `SingleVariable`-in-`LessThan`; - - `@variable(m, x >= 1)` becomes `SingleVariable`-in-`GreaterThan`. - -One notable difference is that a variable with an upper and lower bound is translated into two constraints, rather than an interval. i.e.: - - `@variable(m, 0 <= x <= 1)` becomes `SingleVariable`-in-`LessThan` *and* `SingleVariable`-in-`GreaterThan`. - -Solvers are not expected to support `AbstractScalarFunction` in `GreaterThan`, -`LessThan`, `EqualTo`, or `Interval` with a nonzero constant in the function. -Constants in the affine function should instead be moved into the parameters of -the corresponding sets. The [`ScalarFunctionConstantNotZero`](@ref) exception -may be thrown in this case. - -### Column Generation - -There is no special interface for column generation. If the solver has a special API for setting -coefficients in existing constraints when adding a new variable, it is possible -to queue modifications and new variables and then call the solver's API once all of the -new coefficients are known. - -### Problem data - -All data passed to the solver should be copied immediately to internal data -structures. Solvers may not modify any input vectors and should assume that -input vectors may be modified by users in the future. This applies, for example, -to the `terms` vector in `ScalarAffineFunction`. Vectors returned to the user, -e.g., via `ObjectiveFunction` or `ConstraintFunction` attributes, should not be -modified by the solver afterwards. The in-place version of `get!` can be used by -users to avoid extra copies in this case. - -### Statuses - -Solver wrappers should document how the low-level statuses map to the MOI -statuses. Statuses like `NEARLY_FEASIBLE_POINT` and `INFEASIBLE_POINT`, are -designed to be used when the solver explicitly indicates that relaxed tolerances -are satisfied or the returned point is infeasible, respectively. - -### Naming - -MOI solver interfaces may be in the same package as the solver itself (either -the C wrapper if the solver is accessible through C, or the Julia code if the -solver is written in Julia, for example). The guideline for naming the file -containing the MOI wrapper is `src/MOI_wrapper.jl` and `test/MOI_wrapper.jl` for -the tests. If the MOI wrapper implementation is spread in several files, they -should be stored in a `src/MOI_wrapper` folder and included by a -`src/MOI_wrapper/MOI_wrapper.jl` file. - -By convention, optimizers should not be exported and should be named -`PackageName.Optimizer`. For example, `CPLEX.Optimizer`, `Gurobi.Optimizer`, and -`Xpress.Optimizer`. - -### Testing guideline - -The skeleton below can be used for the wrapper test file of a solver named `FooBar`. -```julia -using Test - -using MathOptInterface -const MOI = MathOptInterface -const MOIT = MOI.Test -const MOIU = MOI.Utilities -const MOIB = MOI.Bridges - -import FooBar -const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes(FooBar.Optimizer, MOI.Silent() => true) -const OPTIMIZER = MOI.instantiate(OPTIMIZER_CONSTRUCTOR) - -@testset "SolverName" begin - @test MOI.get(OPTIMIZER, MOI.SolverName()) == "FooBar" -end - -@testset "supports_default_copy_to" begin - @test MOIU.supports_default_copy_to(OPTIMIZER, false) - # Use `@test !...` if names are not supported - @test MOIU.supports_default_copy_to(OPTIMIZER, true) -end - -const BRIDGED = MOI.instantiate(OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64) -const CONFIG = MOIT.TestConfig(atol=1e-6, rtol=1e-6) - -@testset "Unit" begin - # Test all the functions included in dictionary `MOI.Test.unittests`, - # except functions "number_threads" and "solve_qcp_edge_cases." - MOIT.unittest( - BRIDGED, - CONFIG, - ["number_threads", "solve_qcp_edge_cases"] - ) -end - -@testset "Modification" begin - MOIT.modificationtest(BRIDGED, CONFIG) -end - -@testset "Continuous Linear" begin - MOIT.contlineartest(BRIDGED, CONFIG) -end - -@testset "Continuous Conic" begin - MOIT.contlineartest(BRIDGED, CONFIG) -end - -@testset "Integer Conic" begin - MOIT.intconictest(BRIDGED, CONFIG) -end -``` - -Test functions like `MOI.Test.unittest` and `MOI.Test.modificationtest` are -wrappers around corresponding dictionaries `MOI.Test.unittests` and -`MOI.Test.modificationtests`. The keys of each dictionary (strings describing -the test) map to functions that take two arguments: an optimizer and a -`MOI.Test.TestConfig` object. Exclude tests by passing a vector of strings -corresponding to the test keys you want to exclude as the third positional -argument to the test function (e.g., `MOI.Test.unittest`). - -Print a list of all keys using `println.(keys(MOI.Test.unittests))` - -The optimizer `BRIDGED` constructed with [`instantiate`](@ref) -automatically bridges constraints that are not supported by `OPTIMIZER` -using the bridges listed in [Bridges](@ref). It is recommended for an -implementation of MOI to only support constraints that are natively supported -by the solver and let bridges transform the constraint to the appropriate form. -For this reason it is expected that tests may not pass if `OPTIMIZER` is used -instead of `BRIDGED`. - -To test that a specific problem can be solved without bridges, a specific test can -be run with `OPTIMIZER` instead of `BRIDGED`. For instance -```julia -@testset "Interval constraints" begin - MOIT.linear10test(OPTIMIZER, CONFIG) -end -``` -checks that `OPTIMIZER` implements support for -[`ScalarAffineFunction`](@ref)-in-[`Interval`](@ref). - -If the wrapper does not support building the model incrementally (i.e. with `add_variable` and `add_constraint`), -then `supports_default_copy_to` can be replaced by `supports_allocate_load` if -appropriate (see [Implementing copy](@ref)). - -### Benchmarking - -To aid the development of efficient solver wrappers, MathOptInterface provides -benchmarking functionality. Benchmarking a wrapper follows a two-step process. - -First, prior to making changes, run and save the benchmark results on a given -benchmark suite as follows: - -```julia -using SolverPackage, MathOptInterface - -const MOI = MathOptInterface - -suite = MOI.Benchmarks.suite() do - SolverPackage.Optimizer() -end - -MOI.Benchmarks.create_baseline( - suite, "current"; directory = "/tmp", verbose = true -) -``` -Use the `exclude` argument to [`Benchmarks.suite`](@ref) to -exclude benchmarks that the solver doesn't support. - -Second, after making changes to the package, re-run the benchmark suite and -compare to the prior saved results: - -```julia -using SolverPackage, MathOptInterface - -const MOI = MathOptInterface - -suite = MOI.Benchmarks.suite() do - SolverPackage.Optimizer() -end - -MOI.Benchmarks.compare_against_baseline( - suite, "current"; directory = "/tmp", verbose = true -) -``` - -This comparison will create a report detailing improvements and regressions. diff --git a/docs/src/apireference.md b/docs/src/apireference.md index 984430f955..aa870d334c 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -1084,7 +1084,7 @@ Utilities.side_dimension_for_vectorized_dimension ## Benchmarks Functions to help benchmark the performance of solver wrappers. See -[Benchmarking](@ref) for more details. +[The Benchmarks submodule](@ref) for more details. ```@docs Benchmarks.suite @@ -1095,7 +1095,7 @@ Benchmarks.compare_against_baseline ## File Formats Functions to help read and write MOI models to/from various file formats. See -[File Formats](@ref) for more details. +[The FileFormats submodule](@ref) for more details. ```@docs FileFormats.Model diff --git a/docs/src/index.md b/docs/src/index.md index 4fc5df6573..30fce0facc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,8 +1,84 @@ # MathOptInterface -[MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) is a standardized API for mathematical optimization solvers. +Each mathematical optimization solver API has its own concepts and data +structures for representing optimization models and obtaining results. +However, it is often desirable to represent an instance of an optimization +problem at a higher level so that it is easy to try using different solvers. -```@contents -Pages = ["apimanual.md", "apireference.md", "testing.md"] -Depth = 3 +[MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) (MOI) is +an abstraction layer designed to provide a unified interface to mathematical +optimization solvers so that users do not need to understand multiple +solver-specific APIs. + +MOI can be used directly, or through a higher-level modeling interface like +[JuMP](https://github.com/jump-dev/JuMP.jl). + +## Background + +MOI has been designed to replace [MathProgBase](https://github.com/JuliaOpt/MathProgBase.jl), +which was been used by modeling packages such as [JuMP](https://github.com/jump-dev/JuMP.jl) +and [Convex.jl](https://github.com/jump-dev/Convex.jl). + +This second-generation abstraction layer addresses a number of limitations of +MathProgBase. MOI is designed to: +- Be simple and extensible, unifying linear, quadratic, and conic optimization, + and seamlessly facilitate extensions to essentially arbitrary constraints and + functions (e.g., indicator constraints, complementarity constraints, and + piecewise-linear functions) +- Be fast by allowing access to a solver's in-memory representation of a problem + without writing intermediate files (when possible) and by using multiple + dispatch and avoiding requiring containers of nonconcrete types +- Allow a solver to return multiple results (e.g., a pool of solutions) +- Allow a solver to return extra arbitrary information via attributes (e.g., + variable- and constraint-wise membership in an irreducible inconsistent subset + for infeasibility analysis) +- Provide a greatly expanded set of status codes explaining what happened during + the optimization procedure +- Enable a solver to more precisely specify which problem classes it supports +- Enable both primal and dual warm starts +- Enable adding and removing both variables and constraints by indices that are + not required to be consecutive +- Enable any modification that the solver supports to an existing model +- Avoid requiring the solver wrapper to store an additional copy of the problem + data + +## Sections of this documentation + +There are two main sections to this documentation. + +The manual introduces the concepts needed to understand MOI and gives a +high-level picture of how all of the pieces fit together. The primary focus of +[Basic usage](@ref) is on MOI from the perspective of a user of the interface. +The section [Advanced usage](@ref) provides more detail on advanced topics +such as [Duality](@ref). The manual also has a section on [Implementing a solver interface](@ref). + +In addition to the basic API, MathOptInterface.jl contains a number of +submodules to help users interact with a model in MathOptInterface form. These +include: + - [The `Benchmarks` submodule](@ref) + - [The `Bridges` submodule](@ref) + - [The `FileFormats` submodule](@ref) + - [The `Utilities` submodule](@ref) + - [The `Test` submodule](@ref) + +The [API Reference](@ref) page lists the complete API. + +## Further reading + +A [paper describing the design and features of MathOptInterface](https://arxiv.org/abs/2002.03447) +is available on [arXiv](https://arxiv.org). + +If you find MathOptInterface useful in your work, we kindly request that you +cite the following paper: +``` +@misc{ + legat2020mathoptinterface, + title = {MathOptInterface: a data structure for mathematical optimization problems}, + author = {Beno{\^i}t Legat and Oscar Dowson and Joaquim Dias Garcia and Miles Lubin}, + year = {2020}, + eprint = {2002.03447}, + archivePrefix = {arXiv}, + primaryClass = {math.OC}, + url = {https://arxiv.org/abs/2002.03447}, +} ``` diff --git a/docs/src/manual/Benchmarks.md b/docs/src/manual/Benchmarks.md new file mode 100644 index 0000000000..9ab3cc5b5b --- /dev/null +++ b/docs/src/manual/Benchmarks.md @@ -0,0 +1,52 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# The `Benchmarks` submodule + +To aid the development of efficient solver wrappers, MathOptInterface provides +benchmarking functionality. Benchmarking a wrapper follows a two-step process. + +First, prior to making changes, run and save the benchmark results on a given +benchmark suite as follows: + +```julia +using SolverPackage # Replace with your choice of solver. + +using MathOptInterface +const MOI = MathOptInterface + +suite = MOI.Benchmarks.suite() do + SolverPackage.Optimizer() +end + +MOI.Benchmarks.create_baseline( + suite, "current"; directory = "/tmp", verbose = true +) +``` +Use the `exclude` argument to [`Benchmarks.suite`](@ref) to +exclude benchmarks that the solver doesn't support. + +Second, after making changes to the package, re-run the benchmark suite and +compare to the prior saved results: + +```julia +using SolverPackage, MathOptInterface + +const MOI = MathOptInterface + +suite = MOI.Benchmarks.suite() do + SolverPackage.Optimizer() +end + +MOI.Benchmarks.compare_against_baseline( + suite, "current"; directory = "/tmp", verbose = true +) +``` + +This comparison will create a report detailing improvements and regressions. diff --git a/docs/src/manual/Bridges.md b/docs/src/manual/Bridges.md new file mode 100644 index 0000000000..5e3e9d130f --- /dev/null +++ b/docs/src/manual/Bridges.md @@ -0,0 +1,81 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# The `Bridges` submodule + +A constraint can often be written in a number of equivalent formulations. For +example, the constraint ``l \le a^\top x \le u`` +([`ScalarAffineFunction`](@ref)-in-[`Interval`](@ref)) could be re-formulated as +two constraints: ``a^\top x \ge l`` ([`ScalarAffineFunction`](@ref)-in-[`GreaterThan`](@ref)) +and ``a^\top x \le u`` (`ScalarAffineFunction`-in-`LessThan`). An alternative +re-formulation is to add a dummy variable `y` with the constraints ``l \le y \le u`` +([`SingleVariable`](@ref)-in-[`Interval`](@ref)) and ``a^\top x - y = 0`` +([`ScalarAffineFunction`](@ref)-in-[`EqualTo`](@ref)). + +To avoid each solver having to code these transformations manually, +MathOptInterface provides *bridges*. A bridge is a small transformation from one +constraint type to another (potentially collection of) constraint type. Because +these bridges are included in MathOptInterface, they can be re-used by any +optimizer. Some bridges also implement constraint modifications and constraint +primal and dual translations. + +For example, the `SplitIntervalBridge` defines the reformulation of a +`ScalarAffineFunction`-in-`Interval` constraint into a +`ScalarAffineFunction`-in-`GreaterThan` and a +`ScalarAffineFunction`-in-`LessThan` constraint. `SplitInterval` is the +bridge optimizer that applies the `SplitIntervalBridge` rewriting rule. Given +an optimizer `optimizer` implementing `ScalarAffineFunction`-in-`GreaterThan` +and `ScalarAffineFunction`-in-`LessThan`, the optimizer +```jldoctest; setup=:(model = MOI.Utilities.Model{Float64}()) +optimizer = MOI.Bridges.Constraint.SplitInterval{Float64}(model) +MOI.supports_constraint( + optimizer, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64} +) + +# output + +true +``` +will additionally support [`ScalarAffineFunction`](@ref)-in-[`Interval`](@ref). +Note that these [`Bridges.Constraint.SingleBridgeOptimizer`](@ref)s are mainly +used for testing bridges. + +It is recommended to rather use [`Bridges.full_bridge_optimizer`](@ref) which +automatically selects the appropriate constraint bridges for unsupported +constraints. +```julia +optimizer = MOI.Bridges.full_bridge_optimizer(model, Float64) +``` + +### Variable reformulations + +A variable is often created constrained in a set unsupported by the solver while +it could be parametrized by variables constrained in supported sets. +For example, the [`Bridges.Variable.VectorizeBridge`](@ref) defines the +reformulation of a constrained variable in [`GreaterThan`](@ref) into a +constrained vector of one variable in [`Nonnegatives`](@ref). +The `Bridges.Variable.Vectorize` is the bridge optimizer that applies the +[`Bridges.Variable.VectorizeBridge`](@ref) rewriting rule. Given an optimizer +`optimizer` implementing constrained variables in [`Nonnegatives`](@ref), +the optimizer +```jldoctest; setup=:(model = MOI.Utilities.Model{Float64}()) +optimizer = MOI.Bridges.Variable.Vectorize{Float64}(model) +MOI.supports_add_constrained_variable(optimizer, MOI.GreaterThan{Float64}) + +# output + +true +``` +will additionally support constrained variables in [`GreaterThan`](@ref). +Note that these [`Bridges.Variable.SingleBridgeOptimizer`](@ref) are mainly +used for testing bridges. + +It is recommended to rather use [`Bridges.full_bridge_optimizer`](@ref), which +automatically selects the appropriate bridges for unsupported constrained +variables. diff --git a/docs/src/manual/FileFormats.md b/docs/src/manual/FileFormats.md new file mode 100644 index 0000000000..eab9945ccc --- /dev/null +++ b/docs/src/manual/FileFormats.md @@ -0,0 +1,67 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# The `FileFormats` submodule + +The `FileFormats` module provides functionality for reading and writing MOI +models using [`write_to_file`](@ref) and [`read_from_file`](@ref). + +To write a model `src` to a MathOptFormat file, use: +```julia +src = # ... +dest = FileFormats.Model(format = FileFormats.FORMAT_MOF) +MOI.copy_to(dest, src) +MOI.write_to_file(dest, "file.mof.json") +``` +The list of supported formats is given by the [`FileFormats.FileFormat`](@ref) +enum. + +Instead of the `format` keyword, you can also use the `filename` keyword +argument to [`FileFormats.Model`](@ref). This will attempt to automatically +guess the format from the file extension. For example: +```julia +src = # ... +filename = "my_model.cbf.gz" +dest = FileFormats.Model(filename = filename) +MOI.copy_to(dest, src) +MOI.write_to_file(dest, filename) + +src_2 = FileFormats.Model(filename = filename) +MOI.read_from_file(src_2, filename) +``` +Note how the compression format (GZip) is also automatically detected from the +filename. + +In some cases `src` may contain constraints that are not supported by the file +format (e.g., the CBF format supports integer variables but not binary). If so, +you should copy `src` to a bridged model using [`Bridges.full_bridge_optimizer`](@ref): +```julia +src = # ... conic model ... +dest = FileFormats.Model(format = FileFormats.FORMAT_CBF) +bridged = MOI.Bridges.full_bridge_optimizer(dest, Float64) +MOI.copy_to(bridged, src) +MOI.write_to_file(dest, "my_model.cbf") +``` +You should also note that even after bridging, it may still not be possible to +write the model to file because of unsupported constraints (e.g., PSD variables +in the LP file format). + +In addition to [`write_to_file`](@ref) and [`read_from_file`](@ref), you can +read and write directly from `IO` streams using `Base.write` and `Base.read!`: +```julia +src = # ... +io = IOBuffer() +dest = FileFormats.Model(format = FileFormats.FORMAT_MPS) +MOI.copy_to(dest, src) +write(io, dest) + +seekstart(io) +src_2 = FileFormats.Model(format = FileFormats.FORMAT_MPS) +read!(io, src_2) +``` diff --git a/docs/src/testing.md b/docs/src/manual/Test.md similarity index 95% rename from docs/src/testing.md rename to docs/src/manual/Test.md index 3d3dfc191f..4513561168 100644 --- a/docs/src/testing.md +++ b/docs/src/manual/Test.md @@ -1,4 +1,13 @@ -# Testing +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# The `Test` submodule All solvers use the tests in this repository as extra correctness tests for themselves. If we find a bug in one solver, instead of adding a test to that particular repository, we add it here so that all solvers can benefit. diff --git a/docs/src/manual/Utilities.md b/docs/src/manual/Utilities.md new file mode 100644 index 0000000000..b6c98b6bdb --- /dev/null +++ b/docs/src/manual/Utilities.md @@ -0,0 +1,12 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# The `Utilities` submodule + +TODO: document the utilities submodule diff --git a/docs/src/manual/advanced_usage.md b/docs/src/manual/advanced_usage.md new file mode 100644 index 0000000000..8fc7e59692 --- /dev/null +++ b/docs/src/manual/advanced_usage.md @@ -0,0 +1,186 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# Advanced usage + +## Duality + +Conic duality is the starting point for MOI's duality conventions. When all +functions are affine (or coordinate projections), and all constraint sets are +closed convex cones, the model may be called a conic optimization problem. + +For a minimization problem in geometric conic form, the primal is: +```math +\begin{align} +& \min_{x \in \mathbb{R}^n} & a_0^T x + b_0 +\\ +& \;\;\text{s.t.} & A_i x + b_i & \in \mathcal{C}_i & i = 1 \ldots m +\end{align} +``` +and the dual is a maximization problem in standard conic form: +```math +\begin{align} +& \max_{y_1, \ldots, y_m} & -\sum_{i=1}^m b_i^T y_i + b_0 +\\ +& \;\;\text{s.t.} & a_0 - \sum_{i=1}^m A_i^T y_i & = 0 +\\ +& & y_i & \in \mathcal{C}_i^* & i = 1 \ldots m +\end{align} +``` +where each ``\mathcal{C}_i`` is a closed convex cone and ``\mathcal{C}_i^*`` is +its dual cone. + +For a maximization problem in geometric conic form, the primal is: +```math +\begin{align} +& \max_{x \in \mathbb{R}^n} & a_0^T x + b_0 +\\ +& \;\;\text{s.t.} & A_i x + b_i & \in \mathcal{C}_i & i = 1 \ldots m +\end{align} +``` +and the dual is a minimization problem in standard conic form: +```math +\begin{align} +& \min_{y_1, \ldots, y_m} & \sum_{i=1}^m b_i^T y_i + b_0 +\\ +& \;\;\text{s.t.} & a_0 + \sum_{i=1}^m A_i^T y_i & = 0 +\\ +& & y_i & \in \mathcal{C}_i^* & i = 1 \ldots m +\end{align} +``` + +A linear inequality constraint ``a^T x + b \ge c`` should be interpreted as +``a^T x + b - c \in \mathbb{R}_+``, and similarly ``a^T x + b \le c`` should be +interpreted as ``a^T x + b - c \in \mathbb{R}_-``. Variable-wise constraints +should be interpreted as affine constraints with the appropriate identity +mapping in place of ``A_i``. + +For the special case of minimization LPs, the MOI primal form can be stated as: +```math +\begin{align} +& \min_{x \in \mathbb{R}^n} & a_0^T x &+ b_0 +\\ +& \;\;\text{s.t.} +&A_1 x & \ge b_1\\ +&& A_2 x & \le b_2\\ +&& A_3 x & = b_3 +\end{align} +``` + +By applying the stated transformations to conic form, taking the dual, and +transforming back into linear inequality form, one obtains the following dual: +```math +\begin{align} +& \max_{y_1,y_2,y_3} & b_1^Ty_1 + b_2^Ty_2 + b_3^Ty_3 &+ b_0 +\\ +& \;\;\text{s.t.} +&A_1^Ty_1 + A_2^Ty_2 + A_3^Ty_3 & = a_0\\ +&& y_1 &\ge 0\\ +&& y_2 &\le 0 +\end{align} +``` + +For maximization LPs, the MOI primal form can be stated as: +```math +\begin{align} +& \max_{x \in \mathbb{R}^n} & a_0^T x &+ b_0 +\\ +& \;\;\text{s.t.} +&A_1 x & \ge b_1\\ +&& A_2 x & \le b_2\\ +&& A_3 x & = b_3 +\end{align} +``` +and similarly, the dual is: +```math +\begin{align} +& \min_{y_1,y_2,y_3} & -b_1^Ty_1 - b_2^Ty_2 - b_3^Ty_3 &+ b_0 +\\ +& \;\;\text{s.t.} +&A_1^Ty_1 + A_2^Ty_2 + A_3^Ty_3 & = -a_0\\ +&& y_1 &\ge 0\\ +&& y_2 &\le 0 +\end{align} +``` + +!!! warn + For the LP case is that the signs of the feasible duals depend only on the + sense of the inequality and not on the objective sense. + +### Duality and scalar product + +The scalar product is different from the canonical one for the sets +[`PositiveSemidefiniteConeTriangle`](@ref), [`LogDetConeTriangle`](@ref), +[`RootDetConeTriangle`](@ref). + +If the set ``C_i`` of the section [Duality](@ref) is one of these three cones, +then the rows of the matrix ``A_i`` corresponding to off-diagonal entries are +twice the value of the `coefficients` field in the [`VectorAffineFunction`](@ref) +for the corresponding rows. See [`PositiveSemidefiniteConeTriangle`](@ref) for +details. + +### Dual for problems with quadratic functions + +Given a problem with quadratic functions: +```math +\begin{align*} +& \min_{x \in \mathbb{R}^n} & \frac{1}{2}x^TQ_0x + a_0^T x + b_0 +\\ +& \;\;\text{s.t.} & \frac{1}{2}x^TQ_ix + a_i^T x + b_i & \in \mathcal{C}_i & i = 1 \ldots m +\end{align*} +``` +Consider the Lagrangian function +```math +L(x, y) = \frac{1}{2}x^TQ_0x + a_0^T x + b_0 - \sum_{i = 1}^m y_i (\frac{1}{2}x^TQ_ix + a_i^T x + b_i) +``` +A pair of primal-dual variables $(x^\star, y^\star)$ is optimal if +* ``x^\star`` is a minimizer of + ```math + \min_{x \in \mathbb{R}^n} L(x, y^\star). + ``` + That is, + ```math + 0 = \nabla_x L(x, y^\star) = Q_0x + a_0 - \sum_{i = 1}^m y_i^\star (Q_ix + a_i). + ``` +* and ``y^\star`` is a maximizer of + ```math + \max_{y_i \in \mathcal{C}_i^*} L(x^\star, y). + ``` + That is, for all ``i = 1, \ldots, m``, ``\frac{1}{2}x^TQ_ix + a_i^T x + b_i`` is + either zero or in the normal cone of ``\mathcal{C}_i^*`` at ``y^\star``. + For instance, if ``\mathcal{C}_i`` is ``\{ x \in \mathbb{R} : x \le 0 \}``, it means that + if ``\frac{1}{2}x^TQ_ix + a_i^T x + b_i`` is nonzero then ``\lambda_i = 0``, + this is the classical complementary slackness condition. + +If ``\mathcal{C}_i`` is a vector set, the discussion remains valid with +``y_i(\frac{1}{2}x^TQ_ix + a_i^T x + b_i)`` replaced with the scalar product +between ``y_i`` and the vector of scalar-valued quadratic functions. + +!!! note + For quadratic programs with only affine constraints, the optimality condition + ``\nabla_x L(x, y^\star) = 0`` can be simplified as follows + ```math + 0 = \nabla_x L(x, y^\star) = Q_0x + a_0 - \sum_{i = 1}^m y_i^\star a_i + ``` + which gives + ```math + Q_0x = \sum_{i = 1}^m y_i^\star a_i - a_0 + ``` + The Lagrangian function + ```math + L(x, y) = \frac{1}{2}x^TQ_0x + a_0^T x + b_0 - \sum_{i = 1}^m y_i (a_i^T x + b_i) + ``` + can be rewritten as + ```math + L(x, y) = \frac{1}{2}x^TQ_0x - (\sum_{i = 1}^m y_i a_i^T - a_0^T) x + b_0 - \sum_{i = 1}^m y_i (a_i^T x + b_i) + ``` + which, using the optimality condition ``\nabla_x L(x, y^\star) = 0``, can be simplified as + ```math + L(x, y) = -\frac{1}{2}x^TQ_0x + b_0 - \sum_{i = 1}^m y_i (a_i^T x + b_i) + ``` diff --git a/docs/src/manual/basic_usage.md b/docs/src/manual/basic_usage.md new file mode 100644 index 0000000000..fa64900908 --- /dev/null +++ b/docs/src/manual/basic_usage.md @@ -0,0 +1,832 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# Basic usage + +!!! note + MOI does not export functions, but for brevity we often omit qualifying + names with the MOI module. Best practice is to have + ```julia + using MathOptInterface + const MOI = MathOptInterface + ``` + and prefix all MOI methods with `MOI.` in user code. If a name is also + available in base Julia, we always explicitly use the module prefix, for + example, with `MOI.get`. + +## Standard form problem + +MathOptInterface represents optimization problems in the standard form: +```math +\begin{align} + & \min_{x \in \mathbb{R}^n} & f_0(x) + \\ + & \;\;\text{s.t.} & f_i(x) & \in \mathcal{S}_i & i = 1 \ldots m +\end{align} +``` +where: +* the functions ``f_0, f_1, \ldots, f_m`` are specified by + [`AbstractFunction`](@ref) objects +* the sets ``\mathcal{S}_1, \ldots, \mathcal{S}_m`` are specified by + [`AbstractSet`](@ref) objects + +### Functions + +MOI defines some commonly used functions, but the interface is extensible to +other sets recognized by the solver. The current function types are: + +* **[`SingleVariable`](@ref)**: ``x_j``, i.e., projection onto a single + coordinate defined by a variable index ``j`` +* **[`VectorOfVariables`](@ref)**: projection onto multiple coordinates (i.e., + extracting a subvector) +* **[`ScalarAffineFunction`](@ref)**: ``a^T x + b``, where ``a`` is a vector and + ``b`` scalar +* **[`VectorAffineFunction`](@ref)**: ``A x + b``, where ``A`` is a matrix and + ``b`` is a vector +* **[`ScalarQuadraticFunction`](@ref)**: ``\frac{1}{2} x^T Q x + a^T x + b``, + where ``Q`` is a symmetric matrix, ``a`` is a vector, and ``b`` is a constant +* **[`VectorQuadraticFunction`](@ref)**: a vector of scalar-valued quadratic + functions + +Extensions for nonlinear programming are present but not yet well documented. + +### Sets + +MOI defines some commonly used sets, but the interface is extensible to other +sets recognized by the solver. The current set types are: + +* **[`LessThan(upper)`](@ref MathOptInterface.LessThan)**: + ``\{ x \in \mathbb{R} : x \le \mbox{upper} \}`` +* **[`GreaterThan(lower)`](@ref MathOptInterface.GreaterThan)**: + ``\{ x \in \mathbb{R} : x \ge \mbox{lower} \}`` +* **[`EqualTo(value)`](@ref MathOptInterface.GreaterThan)**: + ``\{ x \in \mathbb{R} : x = \mbox{value} \}`` +* **[`Interval(lower, upper)`](@ref MathOptInterface.Interval)**: + ``\{ x \in \mathbb{R} : x \in [\mbox{lower},\mbox{upper}] \}`` +* **[`Reals(dimension)`](@ref MathOptInterface.Reals)**: + ``\mathbb{R}^\mbox{dimension}`` +* **[`Zeros(dimension)`](@ref MathOptInterface.Zeros)**: ``0^\mbox{dimension}`` +* **[`Nonnegatives(dimension)`](@ref MathOptInterface.Nonnegatives)**: + ``\{ x \in \mathbb{R}^\mbox{dimension} : x \ge 0 \}`` +* **[`Nonpositives(dimension)`](@ref MathOptInterface.Nonpositives)**: + ``\{ x \in \mathbb{R}^\mbox{dimension} : x \le 0 \}`` +* **[`NormInfinityCone(dimension)`](@ref MathOptInterface.NormInfinityCone)**: + ``\{ (t,x) \in \mathbb{R}^\mbox{dimension} : t \ge \lVert x \rVert_\infty = \max_i \lvert x_i \rvert \}`` +* **[`NormOneCone(dimension)`](@ref MathOptInterface.NormOneCone)**: + ``\{ (t,x) \in \mathbb{R}^\mbox{dimension} : t \ge \lVert x \rVert_1 = \sum_i \lvert x_i \rvert \}`` +* **[`SecondOrderCone(dimension)`](@ref MathOptInterface.SecondOrderCone)**: + ``\{ (t,x) \in \mathbb{R}^\mbox{dimension} : t \ge \lVert x \rVert_2 \}`` +* **[`RotatedSecondOrderCone(dimension)`](@ref MathOptInterface.RotatedSecondOrderCone)**: + ``\{ (t,u,x) \in \mathbb{R}^\mbox{dimension} : 2tu \ge \lVert x \rVert_2^2, t,u \ge 0 \}`` +* **[`GeometricMeanCone(dimension)`](@ref MathOptInterface.GeometricMeanCone)**: + ``\{ (t,x) \in \mathbb{R}^{n+1} : x \ge 0, t \le \sqrt[n]{x_1 x_2 \cdots x_n} \}`` + where ``n`` is ``dimension - 1`` +* **[`ExponentialCone()`](@ref MathOptInterface.ExponentialCone)**: + ``\{ (x,y,z) \in \mathbb{R}^3 : y \exp (x/y) \le z, y > 0 \}`` +* **[`DualExponentialCone()`](@ref MathOptInterface.DualExponentialCone)**: + ``\{ (u,v,w) \in \mathbb{R}^3 : -u \exp (v/u) \le exp(1) w, u < 0 \}`` +* **[`PowerCone(exponent)`](@ref MathOptInterface.PowerCone)**: + ``\{ (x,y,z) \in \mathbb{R}^3 : x^\mbox{exponent} y^{1-\mbox{exponent}} \ge |z|, x,y \ge 0 \}`` +* **[`DualPowerCone(exponent)`](@ref MathOptInterface.DualPowerCone)**: + ``\{ (u,v,w) \in \mathbb{R}^3 : \frac{u}{\mbox{exponent}}^\mbox{exponent}\frac{v}{1-\mbox{exponent}}^{1-\mbox{exponent}} \ge |w|, u,v \ge 0 \}`` +* **[`RelativeEntropyCone(dimension)`](@ref MathOptInterface.RelativeEntropyCone)**: + ``\{ (u, v, w) \in \mathbb{R}^\mbox{dimension} : u \ge \sum_i w_i \log (\frac{w_i}{v_i}), v_i \ge 0, w_i \ge 0 \}`` +* **[`NormSpectralCone(row_dim, column_dim)`](@ref MathOptInterface.NormSpectralCone)**: + ``\{ (t, X) \in \mathbb{R}^{1 + \mbox{row_dim} \times \mbox{column_dim}} : t \ge \sigma_1(X), X \mbox{is a matrix with row_dim rows and column_dim columns} \}`` +* **[`NormNuclearCone(row_dim, column_dim)`](@ref MathOptInterface.NormNuclearCone)**: + ``\{ (t, X) \in \mathbb{R}^{1 + \mbox{row_dim} \times \mbox{column_dim}} : t \ge \sum_i \sigma_i(X), X \mbox{is a matrix with row_dim rows and column_dim columns} \}`` +* **[`PositiveSemidefiniteConeTriangle(dimension)`](@ref MathOptInterface.PositiveSemidefiniteConeTriangle)**: + ``\{ X \in \mathbb{R}^{\mbox{dimension}(\mbox{dimension}+1)/2} : X \mbox{is the upper triangle of a PSD matrix} \}`` +* **[`PositiveSemidefiniteConeSquare(dimension)`](@ref MathOptInterface.PositiveSemidefiniteConeSquare)**: + ``\{ X \in \mathbb{R}^{\mbox{dimension}^2} : X \mbox{is a PSD matrix} \}`` +* **[`LogDetConeTriangle(dimension)`](@ref MathOptInterface.LogDetConeTriangle)**: + ``\{ (t,u,X) \in \mathbb{R}^{2+\mbox{dimension}(1+\mbox{dimension})/2} : t \le u\log(\det(X/u)), X \mbox{is the upper triangle of a PSD matrix}, u > 0 \}`` +* **[`LogDetConeSquare(dimension)`](@ref MathOptInterface.LogDetConeSquare)**: + ``\{ (t,u,X) \in \mathbb{R}^{2+\mbox{dimension}^2} : t \le u \log(\det(X/u)), X \mbox{is a PSD matrix}, u > 0 \}`` +* **[`RootDetConeTriangle(dimension)`](@ref MathOptInterface.RootDetConeTriangle)**: + ``\{ (t,X) \in \mathbb{R}^{1+\mbox{dimension}(1+\mbox{dimension})/2} : t \le det(X)^{1/\mbox{dimension}}, X \mbox{is the upper triangle of a PSD matrix} \}`` +* **[`RootDetConeSquare(dimension)`](@ref MathOptInterface.RootDetConeSquare)**: + ``\{ (t,X) \in \mathbb{R}^{1+\mbox{dimension}^2} : t \le \det(X)^{1/\mbox{dimension}}, X \mbox{is a PSD matrix} \}`` +* **[`Integer()`](@ref MathOptInterface.Integer)**: ``\mathbb{Z}`` +* **[`ZeroOne()`](@ref MathOptInterface.ZeroOne)**: ``\{ 0, 1 \}`` +* **[`Semicontinuous(lower,upper)`](@ref MathOptInterface.Semicontinuous)**: + ``\{ 0\} \cup [lower,upper]`` +* **[`Semiinteger(lower,upper)`](@ref MathOptInterface.Semiinteger)**: + ``\{ 0\} \cup \{lower,lower+1,\ldots,upper-1,upper\}`` + +## The `ModelLike` and `AbstractOptimizer` APIs + +The most significant part of MOI is the definition of the **model API** that is +used to specify an instance of an optimization problem (e.g., by adding +variables and constraints). Objects that implement the model API should inherit +from the [`ModelLike`](@ref) abstract type. + +Notably missing from the model API is the method to solve an optimization +problem. `ModelLike` objects may store an instance (e.g., in memory or backed by +a file format) without being linked to a particular solver. In addition to the +model API, MOI defines [`AbstractOptimizer`](@ref). + +*Optimizers* (or solvers) implement the model API (inheriting from `ModelLike`) +and additionally provide methods to solve the model. + +Through the rest of the manual, `model` is used as a generic `ModelLike`, and +`optimizer` is used as a generic `AbstractOptimizer`. + +Models are constructed by +* adding variables using [`add_variable`](@ref) (or [`add_variables`](@ref)), + see [Adding variables](@ref); +* setting an objective sense and function using [`set`](@ref), + see [Setting an objective](@ref); +* and adding constraints using [`add_constraint`](@ref) (or + [`add_constraints`](@ref)), see [Sets and Constraints](@ref). + +The way the problem is solved by the optimimizer is controlled by +[`AbstractOptimizerAttribute`](@ref)s, see [Solver-specific attributes](@ref). + +## Adding variables + +All variables in MOI are scalar variables. New scalar variables are created with +[`add_variable`](@ref) or [`add_variables`](@ref), which return a [`VariableIndex`](@ref) +or `Vector{VariableIndex}` respectively. [`VariableIndex`](@ref) objects are +type-safe wrappers around integers that refer to a variable in a particular +model. + +!!! note + The integer does not necessarily corresond to the column inside an optimizer! + +One uses [`VariableIndex`](@ref) objects to set and get variable attributes. For +example, the [`VariablePrimalStart`](@ref) attribute is used to provide an +initial starting point for a variable or collection of variables: +```julia +v = MOI.add_variable(model) +MOI.set(model, MOI.VariablePrimalStart(), v, 10.5) +v2 = MOI.add_variables(model, 3) +MOI.set(model, MOI.VariablePrimalStart(), v2, [1.3, 6.8, -4.6]) +``` + +A variable can be deleted from a model with +[`delete(::ModelLike, ::VariableIndex)`](@ref MathOptInterface.delete(::MathOptInterface.ModelLike, ::MathOptInterface.Index)). +Not all models support deleting variables; a [`DeleteNotAllowed`](@ref) error is +thrown if this is not supported. + +## Functions + +MOI defines six functions as listed in the definition of the [Standard form problem](@ref). +The simplest function is [`SingleVariable`](@ref), defined as: +```julia +struct SingleVariable <: AbstractFunction + variable::VariableIndex +end +``` + +If `v` is a [`VariableIndex`](@ref) object, then `SingleVariable(v)` is simply +the scalar-valued function from the complete set of variables in a model that +returns the value of variable `v`. One may also call this function a coordinate +projection, which is more useful for defining constraints than as an objective +function. + +A more interesting function is [`ScalarAffineFunction`](@ref), defined as: +```julia +struct ScalarAffineFunction{T} <: AbstractScalarFunction + terms::Vector{ScalarAffineTerm{T}} + constant::T +end +``` + +The [`ScalarAffineTerm`](@ref) struct defines a variable-coefficient pair: +```julia +struct ScalarAffineTerm{T} + coefficient::T + variable_index::VariableIndex +end +``` + +If `x` is a vector of `VariableIndex` objects, then +```julia +MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([5.0, -2.3], [x[1], x[2]]), 1.0) +``` +represents the function ``5 x_1 - 2.3 x_2 + 1``. + +!!! note + `MOI.ScalarAffineTerm.([5.0, -2.3], [x[1], x[2]])` is a shortcut for + `[MOI.ScalarAffineTerm(5.0, x[1]), MOI.ScalarAffineTerm(-2.3, x[2])]`. This + is Julia's broadcast syntax in action, and is used quite often. + +### Setting an objective + +Objective functions are assigned to a model by setting the [`ObjectiveFunction`](@ref) +attribute. The [`ObjectiveSense`](@ref) attribute is used for setting the +optimization sense. For example, +```julia +x = MOI.add_variables(model, 2) +MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.([5.0, -2.3], [x[1], x[2]]), 1.0), + ) +MOI.set(model, MOI.ObjectiveSense(), MIN_SENSE) +``` +sets the objective to the function just discussed in the minimization sense. + +See [Functions and function modifications](@ref) for the complete list of +functions. + +## Sets and Constraints + +All constraints are specified with [`add_constraint`](@ref) by restricting the +output of some function to a set. The interface allows an arbitrary combination +of functions and sets, but of course solvers may decide to support only a small +number of combinations. + +For example, linear programming solvers should support, at least, combinations +of affine functions with the [`LessThan`](@ref) and [`GreaterThan`](@ref) sets. +These are simply linear constraints. [`SingleVariable`](@ref) functions combined +with these same sets are used to specify upper- and lower-bounds on variables. + +The code example below encodes the linear optimization problem: +```math +\begin{align} +& \max_{x \in \mathbb{R}^2} & 3x_1 + 2x_2 & +\\ +& \;\;\text{s.t.} & x_1 + x_2 &\le 5 +\\ +&& x_1 & \ge 0 +\\ +&&x_2 & \ge -1 +\end{align} +``` + +```julia +x = MOI.add_variables(model, 2) +MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([3.0, 2.0], x), 0.0), +) +MOI.set(model, MOI.ObjectiveSense(), MAX_SENSE) +MOI.add_constraint( + model, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0), + MOI.LessThan(5.0), +) +MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.GreaterThan(0.0)) +MOI.add_constraint(model, MOI.SingleVariable(x[2]), MOI.GreaterThan(-1.0)) +``` + +Besides scalar-valued functions in scalar-valued sets, it's also possible to use +vector-valued functions and sets. + +The code example below encodes the convex optimization problem: +```math +\begin{align} +& \max_{x,y,z \in \mathbb{R}} & y + z & +\\ +& \;\;\text{s.t.} & 3x &= 2 +\\ +&& x & \ge \lVert (y,z) \rVert_2 +\end{align} +``` + +```julia +x,y,z = MOI.add_variables(model, 3) +MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, [y, z]), 0.0), +) +MOI.set(model, ObjectiveSense(), MAX_SENSE) +MOI.add_constraint( + model, + MOI.VectorAffineFunction( + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(3.0, x))], [-2.0] + ), + MOI.Zeros(1), +) +MOI.add_constraint( + model, MOI.VectorOfVariables([x, y, z]), MOI.SecondOrderCone(3) +) +``` + +[TODO Describe ConstraintIndex objects.] + +### Constraints by function-set pairs + +Below is a list of common constraint types and how they are represented +as function-set pairs in MOI. In the notation below, ``x`` is a vector of +decision variables, ``x_i`` is a scalar decision variable, ``\alpha, \beta`` are +scalar constants, ``a, b`` are constant vectors, `A` is a constant matrix and +``\mathbb{R}_+`` (resp. ``\mathbb{R}_-``) is the set of nonnegative (resp. +nonpositive) real numbers. + +#### Linear constraints + +| Mathematical Constraint | MOI Function | MOI Set | +|-------------------------------|------------------------------|----------------| +| ``a^Tx \le \beta`` | `ScalarAffineFunction` | `LessThan` | +| ``a^Tx \ge \alpha`` | `ScalarAffineFunction` | `GreaterThan` | +| ``a^Tx = \beta`` | `ScalarAffineFunction` | `EqualTo` | +| ``\alpha \le a^Tx \le \beta`` | `ScalarAffineFunction` | `Interval` | +| ``x_i \le \beta`` | `SingleVariable` | `LessThan` | +| ``x_i \ge \alpha`` | `SingleVariable` | `GreaterThan` | +| ``x_i = \beta`` | `SingleVariable` | `EqualTo` | +| ``\alpha \le x_i \le \beta`` | `SingleVariable` | `Interval` | +| ``Ax + b \in \mathbb{R}_+^n`` | `VectorAffineFunction` | `Nonnegatives` | +| ``Ax + b \in \mathbb{R}_-^n`` | `VectorAffineFunction` | `Nonpositives` | +| ``Ax + b = 0`` | `VectorAffineFunction` | `Zeros` | + +By convention, solvers are not expected to support nonzero constant terms in the +[`ScalarAffineFunction`](@ref)s the first four rows above, because they are +redundant with the parameters of the sets. For example, ``2x + 1 \le 2`` should +be encoded as ``2x \le 1``. + +Constraints with [`SingleVariable`](@ref) in [`LessThan`](@ref), [`GreaterThan`](@ref), +[`EqualTo`](@ref), or [`Interval`](@ref) sets have a natural interpretation as +variable bounds. As such, it is typically not natural to impose multiple lower- +or upper-bounds on the same variable, and the solver interfaces should throw +respectively [`LowerBoundAlreadySet`](@ref) or [`UpperBoundAlreadySet`](@ref). + +Moreover, adding two [`SingleVariable`](@ref) constraints on the same variable +with the same set is impossible because they share the same index as it is the +index of the variable, see [`ConstraintIndex`](@ref). + +It is natural, however, to impose upper- and lower-bounds separately as two +different constraints on a single variable. The difference between imposing +bounds by using a single [`Interval`](@ref) constraint and by using separate +[`LessThan`](@ref) and [`GreaterThan`](@ref) constraints is that the latter will +allow the solver to return separate dual multipliers for the two bounds, while +the former will allow the solver to return only a single dual for the interval +constraint. + +#### Conic constraints + +| Mathematical Constraint | MOI Function | MOI Set | +|---------------------------------------------------------------|------------------------------|------------------------------------| +| ``\lVert Ax + b\rVert_2 \le c^Tx + d`` | `VectorAffineFunction` | `SecondOrderCone` | +| ``y \ge \lVert x \rVert_2`` | `VectorOfVariables` | `SecondOrderCone` | +| ``2yz \ge \lVert x \rVert_2^2, y,z \ge 0`` | `VectorOfVariables` | `RotatedSecondOrderCone` | +| ``(a_1^Tx + b_1,a_2^Tx + b_2,a_3^Tx + b_3) \in \mathcal{E}`` | `VectorAffineFunction` | `ExponentialCone` | +| ``A(x) \in \mathcal{S}_+`` | `VectorAffineFunction` | `PositiveSemidefiniteConeTriangle` | +| ``B(x) \in \mathcal{S}_+`` | `VectorAffineFunction` | `PositiveSemidefiniteConeSquare` | +| ``x \in \mathcal{S}_+`` | `VectorOfVariables` | `PositiveSemidefiniteConeTriangle` | +| ``x \in \mathcal{S}_+`` | `VectorOfVariables` | `PositiveSemidefiniteConeSquare` | + +where ``\mathcal{E}`` is the exponential cone (see [`ExponentialCone`](@ref)), +``\mathcal{S}_+`` is the set of positive semidefinite symmetric matrices, +``A`` is an affine map that outputs symmetric matrices and +``B`` is an affine map that outputs square matrices. + +#### Quadratic constraints + +| Mathematical Constraint | MOI Function | MOI Set | +|-------------------------------|------------------------------|-------------------------------| +| ``x^TQx + a^Tx + b \ge 0`` | `ScalarQuadraticFunction` | `GreaterThan` | +| ``x^TQx + a^Tx + b \le 0`` | `ScalarQuadraticFunction` | `LessThan` | +| ``x^TQx + a^Tx + b = 0`` | `ScalarQuadraticFunction` | `EqualTo` | +| Bilinear matrix inequality | `VectorQuadraticFunction` | `PositiveSemidefiniteCone...` | + + +#### Discrete and logical constraints + +| Mathematical Constraint | MOI Function | MOI Set | +|--------------------------------------------------------------------------------------------|------------------------|------------------------------------| +| ``x_i \in \mathbb{Z}`` | `SingleVariable` | `Integer` | +| ``x_i \in \{0,1\}`` | `SingleVariable` | `ZeroOne` | +| ``x_i \in \{0\} \cup [l,u]`` | `SingleVariable` | `Semicontinuous` | +| ``x_i \in \{0\} \cup \{l,l+1,\ldots,u-1,u\}`` | `SingleVariable` | `Semiinteger` | +| At most one component of ``x`` can be nonzero | `VectorOfVariables` | `SOS1` | +| At most two components of ``x`` can be nonzero, and if so they must be adjacent components | `VectorOfVariables` | `SOS2` | +| ``y = 1 \implies a^T x \in S`` | `VectorAffineFunction` | `IndicatorSet` | + +## Solving and retrieving the results + +Once an optimizer is loaded with the objective function and all of the +constraints, we can ask the solver to solve the model by calling +[`optimize!`](@ref). +```julia +MOI.optimize!(optimizer) +``` + +The optimization procedure may terminate for a number of reasons. The +[`TerminationStatus`](@ref) attribute of the optimizer returns a +[`TerminationStatusCode`](@ref) object which explains why the solver stopped. +The termination statuses distinguish between proofs of optimality, +infeasibility, local convergence, limits, and termination because of something +unexpected like invalid problem data or failure to converge. A typical usage of +the [`TerminationStatus`](@ref) attribute is as follows: +```julia +status = MOI.get(optimizer, TerminationStatus()) +if status == MOI.OPTIMAL + # Ok, we solved the problem! +else + # Handle other cases. +end +``` + +After checking the [`TerminationStatus`](@ref), one should typically check +[`ResultCount`](@ref). This attribute returns the number of results that the +solver has available to return. *A result is defined as a primal-dual pair, +but either the primal or the dual may be missing from the result.* While the +`OPTIMAL` termination status normally implies that at least one result is +available, other statuses do not. For example, in the case of infeasiblity, a +solver may return no result or a proof of infeasibility. The [`ResultCount`](@ref) +attribute distinguishes between these two cases. + +The [`PrimalStatus`](@ref) and [`DualStatus`](@ref) attributes return a +[`ResultStatusCode`](@ref) that indicates if that component of the result +is present (i.e., not `NO_SOLUTION`) and explains how to interpret the result. + +If `PrimalStatus` is not `NO_SOLUTION`, then the primal may be retrieved with +the [`VariablePrimal`](@ref) attribute: +```julia +MOI.get(optimizer, VariablePrimal(), x) +``` + +If `x` is a [`VariableIndex`](@ref) then the function call returns a scalar, and +if `x` is a `Vector{VariableIndex}` then the call returns a vector of scalars. +`VariablePrimal()` is equivalent to `VariablePrimal(1)`, i.e., the variable +primal vector of the first result. Use `VariablePrimal(N)` to access the `N`th +result. + +See also the attributes [`ConstraintPrimal`](@ref), and [`ConstraintDual`](@ref). + +See [Duality](@ref) for a discussion of the MOI conventions for primal-dual +pairs and certificates. + +!!! note + We omit discussion of how to handle multiple results, i.e., when + `ResultCount` is greater than 1. This is supported in the API but not yet + implemented in any solver. + +### Common status situations + +The sections below describe how to interpret typical or interesting status cases +for three common classes of solvers. The example cases are illustrative, not +comprehensive. Solver wrappers may provide additional information on +how the solver's statuses map to MOI statuses. + +`?` in the tables indicate that multiple different values are possible. + +#### Primal-dual convex solver + +Linear programming and conic optimization solvers fall into this category. + +| What happened? | `TerminationStatus()` | `ResultCount()` | `PrimalStatus()` | `DualStatus()` | +| --------------------------------------- | --------------------- | --------------- | ------------------------------------------- | ------------------------------------------- | +| Proved optimality | `OPTIMAL` | 1 | `FEASIBLE_POINT` | `FEASIBLE_POINT` | +| Proved infeasible | `INFEASIBLE` | 1 | `NO_SOLUTION` | `INFEASIBILITY_CERTIFICATE` | +| Optimal within relaxed tolerances | `ALMOST_OPTIMAL` | 1 | `FEASIBLE_POINT` or `ALMOST_FEASIBLE_POINT` | `FEASIBLE_POINT` or `ALMOST_FEASIBLE_POINT` | +| Detected an unbounded ray of the primal | `DUAL_INFEASIBLE` | 1 | `INFEASIBILITY_CERTIFICATE` | `NO_SOLUTION` | +| Stall | `SLOW_PROGRESS` | 1 | ? | ? | + +#### Global branch-and-bound solvers + +Mixed-integer programming solvers fall into this category. + +| What happened? | `TerminationStatus()` | `ResultCount()` | `PrimalStatus()` | `DualStatus()` | +| ------------------------------------------------ | ------------------------- | --------------- | ------------------ | -------------- | +| Proved optimality | `OPTIMAL` | 1 | `FEASIBLE_POINT` | `NO_SOLUTION` | +| Presolve detected infeasibility or unboundedness | `INFEASIBLE_OR_UNBOUNDED` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | +| Proved infeasibility | `INFEASIBLE` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | +| Timed out (no solution) | `TIME_LIMIT` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | +| Timed out (with a solution) | `TIME_LIMIT` | 1 | `FEASIBLE_POINT` | `NO_SOLUTION` | +| `CPXMIP_OPTIMAL_INFEAS` | `ALMOST_OPTIMAL` | 1 | `INFEASIBLE_POINT` | `NO_SOLUTION` | + +[`CPXMIP_OPTIMAL_INFEAS`](https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.6.1/ilog.odms.cplex.help/refcallablelibrary/macros/CPXMIP_OPTIMAL_INFEAS.html) +is a CPLEX status that indicates that a preprocessed problem was solved to +optimality, but the solver was unable to recover a feasible solution to the +original problem. + +#### Local search solvers + +Nonlinear programming solvers fall into this category. It also includes +non-global tree search solvers like +[Juniper](https://github.com/lanl-ansi/Juniper.jl). + +| What happened? | `TerminationStatus()` | `ResultCount()` | `PrimalStatus()` | `DualStatus()` | +| ------------------------------------------------------ | --------------------------------- | --------------- | ------------------ | ---------------- | +| Converged to a stationary point | `LOCALLY_SOLVED` | 1 | `FEASIBLE_POINT` | `FEASIBLE_POINT` | +| Completed a non-global tree search (with a solution) | `LOCALLY_SOLVED` | 1 | `FEASIBLE_POINT` | `FEASIBLE_POINT` | +| Converged to an infeasible point | `LOCALLY_INFEASIBLE` | 1 | `INFEASIBLE_POINT` | ? | +| Completed a non-global tree search (no solution found) | `LOCALLY_INFEASIBLE` | 0 | `NO_SOLUTION` | `NO_SOLUTION` | +| Iteration limit | `ITERATION_LIMIT` | 1 | ? | ? | +| Diverging iterates | `NORM_LIMIT` or `OBJECTIVE_LIMIT` | 1 | ? | ? | + + +## A complete example: solving a knapsack problem + +We first need to select a solver supporting the given problem (see +[`supports`](@ref) and [`supports_constraint`](@ref)). In this example, we +want to solve a binary-constrained knapsack problem: +`max c'x: w'x <= C, x binary`. Suppose we choose GLPK: +```julia +using GLPK +optimizer = GLPK.Optimizer() +``` +We first define the constants of the problem: +```jldoctest knapsack; setup = :(optimizer = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()); MOI.Utilities.set_mock_optimize!(optimizer, mock -> MOI.Utilities.mock_optimize!(mock, ones(3)))) +c = [1.0, 2.0, 3.0] +w = [0.3, 0.5, 1.0] +C = 3.2 + +# output + +3.2 +``` +We create the variables of the problem and set the objective function: +```jldoctest knapsack +x = MOI.add_variables(optimizer, length(c)) +MOI.set( + optimizer, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(c, x), 0.0), +) +MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + +# output + +MAX_SENSE::OptimizationSense = 1 +``` + +We add the knapsack constraint and integrality constraints: +```jldoctest knapsack +MOI.add_constraint( + optimizer, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0), + MOI.LessThan(C), +) +for x_i in x + MOI.add_constraint(optimizer, MOI.SingleVariable(x_i), MOI.ZeroOne()) +end + +# output + +``` + +We are all set! We can now call [`optimize!`](@ref) and wait for the solver to +find the solution: +```jldoctest knapsack +MOI.optimize!(optimizer) + +# output + +``` + +The first thing to check after optimization is why the solver stopped, e.g., +did it stop because of a time limit or did it stop because it found the optimal +solution? +```jldoctest knapsack +MOI.get(optimizer, MOI.TerminationStatus()) + +# output + + +OPTIMAL::TerminationStatusCode = 1 +``` + +It found the optimal solution! Now let's see what is that solution. +```jldoctest knapsack +MOI.get(optimizer, MOI.PrimalStatus()) + +# output + +FEASIBLE_POINT::ResultStatusCode = 1 +``` + +What is its objective value? +```jldoctest knapsack +MOI.get(optimizer, MOI.ObjectiveValue()) + +# output + +6.0 +``` + +And what is the value of the variables `x`? +```jldoctest knapsack +MOI.get(optimizer, MOI.VariablePrimal(), x) + +# output + +3-element Array{Float64,1}: + 1.0 + 1.0 + 1.0 +``` + +## Problem modification + +In addition to adding and deleting constraints and variables, MathOptInterface +supports modifying, in-place, coefficients in the constraints and the objective +function of a model. These modifications can be grouped into two categories: +modifications which replace the set of function of a constraint with a new set +or function; and modifications which change, in-place, a component of a +function. + +In the following, we detail the various ways this can be +achieved. Readers should note that some solvers will not support problem +modification. + +### Replacements + +First, we discuss how to replace the set or function of a constraint with a new +instance of the same type. + +#### The set of a constraint + +Given a constraint of type `F`-in-`S` (see [Constraints by function-set pairs](@ref) + above for an explanation), we can modify parameters (but not the type) of the + set `S` by replacing it with a new instance of the same type. For example, + given the variable bound ``x \le 1``: +```julia +c = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.0)) +``` +we can modify the set so that the bound now ``x \le 2`` as follows: +```julia +MOI.set(model, MOI.ConstraintSet(), c, MOI.LessThan(2.0)) +``` +where `model` is our [`ModelLike`](@ref) model. However, the following will fail +as the new set ([`GreaterThan`](@ref)) is of a different type to the original +set ([`LessThan`](@ref)): +```julia +MOI.set(model, MOI.ConstraintSet(), c, MOI.GreaterThan(2.0)) # errors +``` +If our constraint is an affine inequality, then this corresponds to modifying +the right-hand side of a constraint in linear programming. + +In some special cases, solvers may support efficiently changing the set of a +constraint (for example, from [`LessThan`](@ref) to [`GreaterThan`](@ref)). For +these cases, MathOptInterface provides the [`transform`](@ref) method. For +example, instead of the error we observed above, the following will +work: +```julia +c2 = MOI.transform(model, c, MOI.GreaterThan(1.0)) +``` + +The [`transform`](@ref) function returns a new constraint index, and the old +constraint index (i.e., `c`) is no longer valid: +```julia +MOI.is_valid(model, c) # false +MOI.is_valid(model, c2) # true +``` +Also note that [`transform`](@ref) cannot be called with a set of the same type; +[`set`](@ref) should be used instead. + +#### The function of a constraint + +Given a constraint of type `F`-in-`S` (see [Constraints by function-set pairs](@ref) +above for an explanation), it is also possible to modify the function of type +`F` by replacing it with a new instance of the same type. For example, given the +variable bound ``x \le 1``: +```julia +c = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.0)) +``` +we can modify the function so that the bound now ``y \le 1`` as follows: +```julia +MOI.set(model, MOI.ConstraintFunction(), c, MOI.SingleVariable(y)) +``` +where `m` is our [`ModelLike`](@ref) model. However, the following will fail as +the new function is of a different type to the original function: +```julia +MOI.set( + model, + MOI.ConstraintFunction(), + c, + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), +) +``` + +### In-place modification + +The second type of problem modifications allow the user to modify, in-place, the +coefficients of a function. Currently, four modifications are supported by +MathOptInterface. They are: + 1. change the constant term in a scalar function; + 2. change the constant term in a vector function; + 3. change the affine coefficients in a scalar function; and + 4. change the affine coefficients in a vector function. + +To distinguish between the replacement of the function with a new instance +(described above) and the modification of an existing function, the in-place +modifications use the [`modify`](@ref) method: +```julia +MOI.modify(model, index, change::AbstractFunctionModification) +``` +[`modify`](@ref) takes three arguments. The first is the [`ModelLike`](@ref) +model `model`, the second is the constraint index, and the third is an instance +of an [`AbstractFunctionModification`](@ref). + +We now detail each of these four in-place modifications. + +#### Constant term in a scalar function + +MathOptInterface supports is the ability to modify the constant term within a +[`ScalarAffineFunction`](@ref) and a [`ScalarQuadraticFunction`](@ref) using +the [`ScalarConstantChange`](@ref) subtype of +[`AbstractFunctionModification`](@ref). This includes the objective function, as +well as the function in a function-pair constraint. + +For example, consider a problem `model` with the objective ``\max 1.0x + 0.0``: +```julia +MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), +) +``` + +We can modify the constant term in the objective function as follows: +```julia +MOI.modify( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarConstantChange(1.0) +) +``` +The objective function will now be ``\max 1.0x + 1.0``. + +#### Constant terms in a vector function + +We can modify the constant terms in a [`VectorAffineFunction`](@ref) or a +[`VectorQuadraticFunction`](@ref) using the [`VectorConstantChange`](@ref) +subtype of [`AbstractFunctionModification`](@ref). + +For example, consider a model with the following +[`VectorAffineFunction`](@ref)-in-[`Nonpositives`](@ref) constraint: +```julia +c = MOI.add_constraint( + model, + MOI.VectorAffineFunction([ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x)), + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(2.0, y)) + ], + [0.0, 0.0], + ), + MOI.Nonpositives(2), +) +``` +We can modify the constant vector in the [`VectorAffineFunction`](@ref) from +`[0.0, 0.0]` to `[1.0, 2.0]` as follows: +```julia +MOI.modify(model, c, MOI.VectorConstantChange([1.0, 2.0]) +) +``` +The constraints are now ``1.0x + 1.0 \le 0.0`` and ``2.0y + 2.0 \le 0.0``. + +#### Affine coefficients in a scalar function + +In addition to modifying the constant terms in a function, we can also modify +the affine variable coefficients in an [`ScalarAffineFunction`](@ref) or a +[`ScalarQuadraticFunction`](@ref) using the [`ScalarCoefficientChange`](@ref) +subtype of [`AbstractFunctionModification`](@ref). + +For example, given the constraint ``1.0x <= 1.0``: +```julia +c = MOI.add_constraint( + model, + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), + MOI.LessThan(1.0), +) +``` +we can modify the coefficient of the `x` variable so that the constraint becomes +``2.0x <= 1.0`` as follows: +```julia +MOI.modify(model, c, MOI.ScalarCoefficientChange(x, 2.0)) +``` + +[`ScalarCoefficientChange`](@ref) can also be used to modify the objective +function by passing an instance of [`ObjectiveFunction`](@ref) instead of the +constraint index `c` as we saw above. + +#### Affine coefficients in a vector function + +Finally, the last modification supported by MathOptInterface is the ability to +modify the affine coefficients of a single variable in a +[`VectorAffineFunction`](@ref) or a [`VectorQuadraticFunction`](@ref) using +the [`MultirowChange`](@ref) subtype of [`AbstractFunctionModification`](@ref). + +For example, given the constraint ``Ax \in \mathbb{R}^2_+``, where +``A = [1.0, 2.0]^\top``: +```julia +c = MOI.add_constraint( + model, + MOI.VectorAffineFunction([ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x)), + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(2.0, x)), + ], + [0.0, 0.0], + ), + MOI.Nonnegatives(2), +) +``` +we can modify the coefficients of the `x` variable so that the `A` matrix +becomes ``A = [3.0, 4.0]^\top`` as follows: +```julia +MOI.modify(model, c, MOI.MultirowChange(x, [3.0, 4.0])) +``` diff --git a/docs/src/manual/implementing.md b/docs/src/manual/implementing.md new file mode 100644 index 0000000000..27b271239a --- /dev/null +++ b/docs/src/manual/implementing.md @@ -0,0 +1,313 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# Implementing a solver interface + +[The interface is designed for multiple dispatch, e.g., attributes, combinations +of sets and functions.] + +## Solver-specific attributes + +Solver-specific attributes should be specified by creating an +[`AbstractOptimizerAttribute`](@ref). For example, inside `MyPackage`, we could +add the following: +```julia +struct PrintLevel <: MOI.AbstractOptimizerAttribute end +function MOI.set(model::Optimizer, ::PrintLevel, level::Int) + # ... set the print level ... +end +``` +Then, the user can write: +```julia +model = MyPackage.Optimizer() +MOI.set(model, MyPackage.PrintLevel(), 0) +``` + +## Supported constrained variables and constraints + +The solver interface should only implement support for variables +constrained on creation (see +[`add_constrained_variable`](@ref)/[`add_constrained_variables`](@ref)) or +constraints that directly map to a structure exploited by the solver algorithm. +There is no need to add support for additional types, this is handled by +[The Bridges submodule](@ref). Furthermore, this allows +[`supports_constraint`](@ref) to indicate which types are exploited by the +solver and hence allows layers such as [`Bridges.LazyBridgeOptimizer`](@ref) +to accurately select the most appropriate transformations. + +As [`add_constrained_variable`](@ref) (resp. [`add_constrained_variables`](@ref)) +falls back to [`add_variable`](@ref) (resp. [`add_variables`](@ref)) followed by +[`add_constraint`](@ref), there is no need to implement this function +if `model` does not require that variables be constrained when they are created. +However, if `model` requires that variables be constrained when they're created, +then it should only implement [`add_constrained_variable`](@ref) and not +[`add_variable`](@ref) nor [`add_constraint`](@ref) for +[`SingleVariable`](@ref)-in-`typeof(set)`. In addition, it should implement +`supports_add_constrained_variables(::Optimizer, ::Type{Reals})` and return +`false` so that these variables are bridged, see +[`supports_add_constrained_variables`](@ref). + +## Handling duplicate coefficients + +Solvers should expect that functions such as `ScalarAffineFunction` and +`VectorQuadraticFunction` may contain duplicate coefficents, for example, +`ScalarAffineFunction([ScalarAffineTerm(x, 1), ScalarAffineTerm(x, 1)], 0.0)`. +These duplicate terms can be aggregated by calling +[`Utilities.canonical`](@ref). + +```jldoctest; setup = :(using MathOptInterface) +x = MathOptInterface.VariableIndex(1) +term = MathOptInterface.ScalarAffineTerm(1, x) +func = MathOptInterface.ScalarAffineFunction([term, term], 0) +func_canon = MathOptInterface.Utilities.canonical(func) +func_canon ≈ MathOptInterface.ScalarAffineFunction( + [MathOptInterface.ScalarAffineTerm(2, x)], 0) + +# output + +true +``` + +## Implementing copy + +Avoid storing extra copies of the problem when possible. This means that solver +wrappers should not use [`Utilities.CachingOptimizer`](@ref) as part of the +wrapper. Instead, do one of the following to load the problem (assuming the +solver wrapper type is called `Optimizer`): + +* If the solver supports loading the problem incrementally, implement + [`add_variable`](@ref), [`add_constraint`](@ref) for supported constraints and + [`set`](@ref) for supported attributes and add: + ```julia + function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...) + return MOI.Utilities.automatic_copy_to(dest, src; kws...) + end + ``` + with + ```julia + MOI.Utilities.supports_default_copy_to(model::Optimizer, copy_names::Bool) = true + ``` + or + ```julia + MOI.Utilities.supports_default_copy_to(model::Optimizer, copy_names::Bool) = !copy_names + ``` + depending on whether the solver support names; see + [`Utilities.supports_default_copy_to`](@ref) for more details. +* If the solver does not support loading the problem incrementally, do not + implement [`add_variable`](@ref) and [`add_constraint`](@ref) as implementing + them would require caching the problem. Let users or JuMP decide whether to + use a `CachingOptimizer` instead. Write either a custom implementation of + [`copy_to`](@ref) or implement the [Allocate-Load API](@ref). If you choose to + implement the Allocate-Load API, + do + ```julia + function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...) + return MOI.Utilities.automatic_copy_to(dest, src; kws...) + end + ``` + with + ```julia + MOI.Utilities.supports_allocate_load(model::Optimizer, copy_names::Bool) = true + ``` + or + ```julia + MOI.Utilities.supports_allocate_load(model::Optimizer, copy_names::Bool) = !copy_names + ``` + depending on whether the solver support names; see + [`Utilities.supports_allocate_load`](@ref) for more details. + + Note that even if both writing a custom implementation of [`copy_to`](@ref) + and implementing the [Allocate-Load API](@ref) requires the user to copy the + model from a cache, the [Allocate-Load API](@ref) allows MOI layers to be + added between the cache and the solver which allows transformations to be + applied without the need for additional caching. For instance, with the + proposed [Light bridges](https://github.com/jump-dev/MathOptInterface.jl/issues/523), + no cache will be needed to store the bridged model when bridges are used by + JuMP so implementing the [Allocate-Load API](@ref) will allow JuMP to use only + one cache instead of two. + +## JuMP mapping + +MOI defines a very general interface, with multiple possible ways to describe +the same constraint. + +This is considered a feature, not a bug. + +MOI is designed to make it possible to experiment with alternative +representations of an optimization problem at both the solving and modeling +level. + +When implementing an interface, it is important to keep in mind that the way the +user can express problems in JuMP is not directly limited by the constraints +which a solver supports via MOI as JuMP performs automatic reformulation](@ref) +via [The Bridges submodule](@ref). + +Therefore, we recommend to only support the constraint types that directly map +to a structure exploited by the solver algorithm. + +The following bullet points show examples of how JuMP constraints are translated +into MOI function-set pairs: + + - `@constraint(m, 2x + y <= 10)` becomes `ScalarAffineFunction`-in-`LessThan` + - `@constraint(m, 2x + y >= 10)` becomes `ScalarAffineFunction`-in-`GreaterThan` + - `@constraint(m, 2x + y == 10)` becomes `ScalarAffineFunction`-in-`EqualTo` + - `@constraint(m, 0 <= 2x + y <= 10)` becomes `ScalarAffineFunction`-in-`Interval` + - `@constraint(m, 2x + y in ArbitrarySet())` becomes + `ScalarAffineFunction`-in-`ArbitrarySet`. + +Variable bounds are handled in a similar fashion: + + - `@variable(m, x <= 1)` becomes `SingleVariable`-in-`LessThan` + - `@variable(m, x >= 1)` becomes `SingleVariable`-in-`GreaterThan` + +One notable difference is that a variable with an upper and lower bound is +translated into two constraints, rather than an interval. i.e.: + + - `@variable(m, 0 <= x <= 1)` becomes `SingleVariable`-in-`LessThan` *and* + `SingleVariable`-in-`GreaterThan`. + +Solvers are not expected to support `AbstractScalarFunction` in `GreaterThan`, +`LessThan`, `EqualTo`, or `Interval` with a nonzero constant in the function. +Constants in the affine function should instead be moved into the parameters of +the corresponding sets. The [`ScalarFunctionConstantNotZero`](@ref) exception +may be thrown in this case. + +## Column Generation + +There is no special interface for column generation. If the solver has a special +API for setting coefficients in existing constraints when adding a new variable, +it is possible to queue modifications and new variables and then call the +solver's API once all of the new coefficients are known. + +## Problem data + +All data passed to the solver should be copied immediately to internal data +structures. Solvers may not modify any input vectors and should assume that +input vectors may be modified by users in the future. This applies, for example, +to the `terms` vector in `ScalarAffineFunction`. Vectors returned to the user, +e.g., via `ObjectiveFunction` or `ConstraintFunction` attributes, should not be +modified by the solver afterwards. The in-place version of `get!` can be used by +users to avoid extra copies in this case. + +## Statuses + +Solver wrappers should document how the low-level statuses map to the MOI +statuses. Statuses like `NEARLY_FEASIBLE_POINT` and `INFEASIBLE_POINT`, are +designed to be used when the solver explicitly indicates that relaxed tolerances +are satisfied or the returned point is infeasible, respectively. + +## Naming + +MOI solver interfaces may be in the same package as the solver itself (either +the C wrapper if the solver is accessible through C, or the Julia code if the +solver is written in Julia, for example). The guideline for naming the file +containing the MOI wrapper is `src/MOI_wrapper.jl` and `test/MOI_wrapper.jl` for +the tests. If the MOI wrapper implementation is spread in several files, they +should be stored in a `src/MOI_wrapper` folder and included by a +`src/MOI_wrapper/MOI_wrapper.jl` file. + +By convention, optimizers should not be exported and should be named +`PackageName.Optimizer`. For example, `CPLEX.Optimizer`, `Gurobi.Optimizer`, and +`Xpress.Optimizer`. + +## Testing guideline + +The skeleton below can be used for the wrapper test file of a solver named +`FooBar`. +```julia +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MOI.Test +const MOIU = MOI.Utilities +const MOIB = MOI.Bridges + +import FooBar +const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes( + FooBar.Optimizer, MOI.Silent() => true +) +const OPTIMIZER = MOI.instantiate(OPTIMIZER_CONSTRUCTOR) + +@testset "SolverName" begin + @test MOI.get(OPTIMIZER, MOI.SolverName()) == "FooBar" +end + +@testset "supports_default_copy_to" begin + @test MOIU.supports_default_copy_to(OPTIMIZER, false) + # Use `@test !...` if names are not supported + @test MOIU.supports_default_copy_to(OPTIMIZER, true) +end + +const BRIDGED = MOI.instantiate( + OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64 +) +const CONFIG = MOIT.TestConfig(atol=1e-6, rtol=1e-6) + +@testset "Unit" begin + # Test all the functions included in dictionary `MOI.Test.unittests`, + # except functions "number_threads" and "solve_qcp_edge_cases." + MOIT.unittest( + BRIDGED, + CONFIG, + ["number_threads", "solve_qcp_edge_cases"] + ) +end + +@testset "Modification" begin + MOIT.modificationtest(BRIDGED, CONFIG) +end + +@testset "Continuous Linear" begin + MOIT.contlineartest(BRIDGED, CONFIG) +end + +@testset "Continuous Conic" begin + MOIT.contlineartest(BRIDGED, CONFIG) +end + +@testset "Integer Conic" begin + MOIT.intconictest(BRIDGED, CONFIG) +end +``` + +Test functions like `MOI.Test.unittest` and `MOI.Test.modificationtest` are +wrappers around corresponding dictionaries `MOI.Test.unittests` and +`MOI.Test.modificationtests`. The keys of each dictionary (strings describing +the test) map to functions that take two arguments: an optimizer and a +`MOI.Test.TestConfig` object. Exclude tests by passing a vector of strings +corresponding to the test keys you want to exclude as the third positional +argument to the test function (e.g., `MOI.Test.unittest`). + +Print a list of all keys using `println.(keys(MOI.Test.unittests))` + +The optimizer `BRIDGED` constructed with [`instantiate`](@ref) +automatically bridges constraints that are not supported by `OPTIMIZER` +using the bridges listed in [Bridges](@ref). It is recommended for an +implementation of MOI to only support constraints that are natively supported +by the solver and let bridges transform the constraint to the appropriate form. +For this reason it is expected that tests may not pass if `OPTIMIZER` is used +instead of `BRIDGED`. + +To test that a specific problem can be solved without bridges, a specific test +can be run with `OPTIMIZER` instead of `BRIDGED`. For instance +```julia +@testset "Interval constraints" begin + MOIT.linear10test(OPTIMIZER, CONFIG) +end +``` +checks that `OPTIMIZER` implements support for +[`ScalarAffineFunction`](@ref)-in-[`Interval`](@ref). + +If the wrapper does not support building the model incrementally (i.e. with +[`add_variable`](@ref) and [`add_constraint`](@ref)), +then [`Utilities.supports_default_copy_to`](@ref) can be replaced by +[`Utilities.supports_allocate_load`](@ref) if appropriate (see +[Implementing copy](@ref)). + diff --git a/src/sets.jl b/src/sets.jl index 44c5c69e60..d159d39f99 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -36,8 +36,11 @@ function dimension end dual_set(s::AbstractSet) Return the dual set of `s`, that is the dual cone of the set. This follows the -definition of duality discussed in [Duals](@ref). -See [Dual cone](https://en.wikipedia.org/wiki/Dual_cone_and_polar_cone) for more information. +definition of duality discussed in [Duality](@ref). + +See [Dual cone](https://en.wikipedia.org/wiki/Dual_cone_and_polar_cone) for more +information. + If the dual cone is not defined it returns an error. ### Examples diff --git a/src/variables.jl b/src/variables.jl index 0e8929d252..267ac51081 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -107,7 +107,7 @@ definition for most models. ## Example -In the standard conic form (see [Duals](@ref)), the variables are grouped into +In the standard conic form (see [Duality](@ref)), the variables are grouped into several cones and the constraints are affine equality constraints. If `Reals` is not one of the cones supported by the solvers then it needs to implement `supports_add_constrained_variables(::Optimizer, ::Type{Reals}) = false`