diff --git a/docs/make.jl b/docs/make.jl index 3d8d953291..d6774c32f6 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -28,6 +28,7 @@ makedocs( "tutorials/example.md", "tutorials/implementing.md", "tutorials/mathprogbase.md", + "tutorials/bridging_constraint.md", ], "Manual" => [ "manual/standard_form.md", diff --git a/docs/src/submodules/Bridges/overview.md b/docs/src/submodules/Bridges/overview.md index 33718113f5..85717a6ae1 100644 --- a/docs/src/submodules/Bridges/overview.md +++ b/docs/src/submodules/Bridges/overview.md @@ -37,6 +37,11 @@ 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. +Several bridges can be used in combination to transform a single constraint +into a form that the solver may understand. Choosing the bridges to use +takes the form of finding a shortest path in the hypergraph of bridges. The +methodology is detailed in [the MOI paper](https://arxiv.org/abs/2002.03447). + ## The three types of bridges There are three types of bridges in MathOptInterface: @@ -47,22 +52,30 @@ There are three types of bridges in MathOptInterface: ### Constraint bridges Constraint bridges convert constraints formulated by the user into an equivalent -form supported by the solver. +form supported by the solver. Constraint bridges are subtypes of +[`Bridges.Constraint.AbstractBridge`](@ref). The equivalent formulation may add constraints (and possibly also variables) in the underlying model. +In particular, constraint bridges can focus on rewriting the function of a +constraint, and do not change the set. Function bridges are subtypes of +[`Bridges.Constraint.AbstractFunctionConversionBridge`](@ref). + Read the [list of implemented constraint bridges](@ref constraint_bridges_ref) for more details on the types of transformations that are available. +Function bridges are [`Bridges.Constraint.ScalarFunctionizeBridge`](@ref) and +[`Bridges.Constraint.VectorFunctionizeBridge`](@ref). ### [Variable bridges](@id variable_bridges) Variable bridges convert variables added by the user, either free with [`add_variable`](@ref)/[`add_variables`](@ref), or constrained with [`add_constrained_variable`](@ref)/[`add_constrained_variables`](@ref), -into an equivalent form supported by the solver. +into an equivalent form supported by the solver. Variable bridges are +subtypes of [`Bridges.Variable.AbstractBridge`](@ref). -Te equivalent formulation may add constraints (and possibly also variables) in +The equivalent formulation may add constraints (and possibly also variables) in the underlying model. Read the [list of implemented variable bridges](@ref variable_bridges_ref) for @@ -71,9 +84,10 @@ more details on the types of transformations that are available. ### Objective bridges Objective bridges convert the [`ObjectiveFunction`](@ref) set by the user into -an equivalent form supported by the solver. +an equivalent form supported by the solver. Objective bridges are +subtypes of [`Bridges.Objective.AbstractBridge`](@ref). -Te equivalent formulation may add constraints (and possibly also variables) in +The equivalent formulation may add constraints (and possibly also variables) in the underlying model. Read the [list of implemented objective bridges](@ref objective_bridges_ref) for diff --git a/docs/src/submodules/Bridges/reference.md b/docs/src/submodules/Bridges/reference.md index 3d134a30bc..6a4f13c882 100644 --- a/docs/src/submodules/Bridges/reference.md +++ b/docs/src/submodules/Bridges/reference.md @@ -37,6 +37,7 @@ Bridges.Variable.unbridged_map ```@docs Bridges.Constraint.AbstractBridge +Bridges.Constraint.AbstractFunctionConversionBridge Bridges.Constraint.SingleBridgeOptimizer Bridges.Constraint.add_all_bridges ``` @@ -57,6 +58,7 @@ Bridges.inverse_adjoint_map_function ### [Bridges implemented](@id constraint_bridges_ref) ```@docs +Bridges.Constraint.FlipSignBridge Bridges.Constraint.GreaterToIntervalBridge Bridges.Constraint.GreaterToLessBridge Bridges.Constraint.LessToIntervalBridge @@ -104,6 +106,7 @@ Bridges.Variable.add_all_bridges ### [Bridges implemented](@id variable_bridges_ref) ```@docs +Bridges.Variable.FlipSignBridge Bridges.Variable.ZerosBridge Bridges.Variable.FreeBridge Bridges.Variable.NonposToNonnegBridge diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 552b27ef47..fcd88befeb 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -49,6 +49,12 @@ Utilities.state Utilities.mode ``` +### Mock optimizer + +```@docs +Utilities.MockOptimizer +``` + ## Printing ```@docs diff --git a/docs/src/tutorials/bridging_constraint.md b/docs/src/tutorials/bridging_constraint.md new file mode 100644 index 0000000000..8e13c725ce --- /dev/null +++ b/docs/src/tutorials/bridging_constraint.md @@ -0,0 +1,318 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# Implementing a constraint bridge + +This guide outlines the basic steps to create a new bridge from a constraint +expressed in the formalism `Function`-in-`Set`. + +## Preliminaries + +First, decide on the set you want to bridge. Then, study its properties: the +most important one is whether the set is scalar or vector, which impacts the +dimensionality of the functions that can be used with the set. + +* A scalar function only has one dimension. MOI defines three types of scalar + functions: a variable ([`SingleVariable`](@ref)), an affine function + ([`ScalarAffineFunction`](@ref)), or a quadratic function + ([`ScalarQuadraticFunction`](@ref)). +* A vector function has several dimensions (at least one). MOI defines three + types of vector functions: several variables ([`VectorOfVariables`](@ref)), + an affine function ([`VectorAffineFunction`](@ref)), or a quadratic function + ([`VectorQuadraticFunction`](@ref)). The main difference with scalar functions + is that the order of dimensions can be very important: for instance, in an + indicator constraint ([`Indicator`](@ref)), the first dimension indicates + whether the constraint about the second dimension is active. + +To explain how to implement a bridge, we present the example of +[`Bridges.Constraint.FlipSignBridge`](@ref). This bridge maps `<=` +([`LessThan`](@ref)) constraints to `>=` ([`GreaterThan`](@ref)) constraints. +This corresponds to reversing the sign of the inequality. We focus on scalar +affine functions (we disregard the cases of a single variable or of quadratic +functions). This example is a simplified version of the code included +in MOI. + +## Four mandatory parts in a constraint bridge + +The first part of a constraint bridge is a new concrete subtype of +[`Bridges.Constraint.AbstractBridge`](@ref). This type must have fields to +store all the new variables and constraints that the bridge will add. +Typically, these types are parametrized by the type of the coefficients in the +model. + +Then, three sets of functions must be defined: + +1. [`Bridges.Constraint.bridge_constraint`](@ref): this function implements the + bridge and creates the required variables and constraints. +2. [`supports_constraint`](@ref): these functions should return `true` when the + combination of function and set is supported by the bridge. By default, the + base implementation always returns `false` and the bridge does not have to + provide this implementation. +3. [`Bridges.added_constrained_variable_types`](@ref) and + [`Bridges.added_constraint_types`](@ref): these functions return the types + of variables and constraints that this bridge adds. They are used to compute + the set of other bridges that are required to use the one you are defining, + if need be. + +More functions can be implemented, for instance to retrieve properties from the +bridge or deleting a bridged constraint. + +### 1. Structure for the bridge + +A typical `struct` behind a bridge depends on the type of the coefficients that +are used for the model (typically `Float64`, but coefficients might also be +integers or complex numbers). + +This structure must hold a reference to all the variables and the constraints +that are created as part of the bridge. + +The type of this structure is used throughout MOI as an identifier for the +bridge. It is passed as argument to most functions related to bridges. + +The best practice is to have the name of this type end with `Bridge`. + +In our example, the bridge should be able to map any +`ScalarAffineFunction{T}`-in-`LessThan{T}` constraint to a single +`ScalarAffineFunction{T}`-in-`GreaterThan{T}` constraint. The affine function +has coefficients of type `T`. The bridge is parametrized with `T`, so that the +constraint that the bridge creates also has coefficients of type `T`. + +```julia +struct SignBridge{T<:Number} <: Bridges.Constraint.AbstractBridge + constraint::ConstraintIndex{ScalarAffineFunction{T}, GreaterThan{T}} +end +``` + +### 2. Bridge creation + +The function [`Bridges.Constraint.bridge_constraint`](@ref) is called whenever +the bridge should be instantiated for a specific model, with the given function +and set. The arguments to `bridge_constraint` are similar to +[`add_constraint`](@ref), with the exception of the first argument: it is the +`Type` of the struct defined in the first step (for our example, +`Type{SignBridge{T}}`). + +`bridge_constraint` returns an instance of the struct defined in the first step. +the first step. + +In our example, the bridge constraint could be defined as: + +```julia +function Bridges.Constraint.bridge_constraint( + ::Type{SignBridge{T}}, # Bridge to use. + model::ModelLike, # Model to which the constraint is being added. + f::ScalarAffineFunction{T}, # Function to rewrite. + s::LessThan{T}, # Set to rewrite. +) where {T} + # Create the variables and constraints required for the bridge. + con = add_constraint(model, -f, GreaterThan(-s.upper)) + + # Return an instance of the bridge type with a reference to all the + # variables and constraints that were created in this function. + return SignBridge(con) +end +``` + +### 3. Supported constraint types + +The function [`supports_constraint`](@ref) determines whether the bridge type +supports a given combination of function and set. + +This function must closely match `bridge_constraint`, because it will not be +called if `supports_constraint` returns `false`. + +```julia +function supports_constraint( + ::Type{SignBridge{T}}, # Bridge to use. + ::Type{ScalarAffineFunction{T}}, # Function to rewrite. + ::Type{LessThan{T}}, # Set to rewrite. +) where {T} + # Do some computation to ensure that the constraint is supported. + # Typically, you can directly return true. + return true +end +``` + +### 4. Metadata about the bridge + +To determine whether a bridge can be used, MOI uses a shortest-path algorithm +that uses the variable types and the constraints that the bridge can create. +This information is communicated from the bridge to MOI using the functions +[`Bridges.added_constrained_variable_types`](@ref) and +[`Bridges.added_constraint_types`](@ref). Both return lists of tuples: +either a list of 1-tuples containing the variable types (typically, `ZeroOne` +or `Integer`) or a list of 2-tuples contained the functions and sets (like +`ScalarAffineFunction{T}`-`GreaterThan`). + +For our example, the bridge does not create any constrained variables, and +only `ScalarAffineFunction{T}`-in-`GreaterThan{T}` constraints: + +```julia +function Bridges.added_constrained_variable_types(::Type{SignBridge{T}}) where {T} + # The bridge does not create variables, return an empty list of tuples: + return Tuple{Type}[] +end + +function Bridges.added_constraint_types(::Type{SignBridge{T}}) where {T} + return Tuple{Type,Type}[ + # One element per F-in-S the bridge creates. + (ScalarAffineFunction{T}, GreaterThan{T}), + ] +end +``` + +A bridge that creates binary variables would rather have this definition of +`added_constrained_variable_types`: + +```julia +function Bridges.added_constrained_variable_types(::Type{SomeBridge{T}}) where {T} + # The bridge only creates binary variables: + return Tuple{Type}[(ZeroOne,)] +end +``` + +## Bridge registration + +For a bridge to be used by MOI, it must be known by MOI. + +### `SingleBridgeOptimizer` + +The first way to do so is to create a single-bridge optimizer. This type of +optimizer wraps another optimizer and adds the possibility to use only one +bridge. It is especially useful when unit testing bridges. + +It is common practice to use the same name as the type defined for the bridge +(`SignBridge`, in our example) without the suffix `Bridge`. + +```julia +const Sign{T,OT<: ModelLike} = + SingleBridgeOptimizer{SignBridge{T}, OT} +``` + +In the context of unit tests, this bridge is used in conjunction with a +[`Utilities.MockOptimizer`](@ref): + +```julia +mock = Utilities.MockOptimizer( + Utilities.UniversalFallback(Utilities.Model{Float64}()), +) +bridged_mock = Sign{Float64}(mock) +``` + +### New bridge for a `LazyBridgeOptimizer` + +Typical user-facing models for MOI are based on +[`Bridges.LazyBridgeOptimizer`](@ref). For instance, this type of model is +returned by [`Bridges.full_bridge_optimizer`](@ref). These models can be +added more bridges by using [`Bridges.add_bridge`](@ref): + +```julia +inner_optimizer = Utilities.Model{Float64}() +optimizer = Bridges.full_bridge_optimizer(inner_optimizer, Float64) +Bridges.add_bridge(optimizer, SignBridge{Float64}) +``` + +## Bridge improvements + +### Attribute retrieval + +Like models, bridges have attributes that can be retrieved using [`get`](@ref) +and [`set`](@ref). The most important ones are the number of variables and +constraints, but also the lists of variables and constraints. + +In our example, we only have one constraint and only have to implement the +[`NumberOfConstraints`](@ref) and [`ListOfConstraintIndices`](@ref) attributes: + +```julia +function get( + ::SignBridge{T}, + ::NumberOfConstraints{ + ScalarAffineFunction{T}, + GreaterThan{T}, + }, +) where {T} + return 1 +end + +function get( + bridge::SignBridge{T}, + ::ListOfConstraintIndices{ + ScalarAffineFunction{T}, + GreaterThan{T}, + }, +) where {T} + return [bridge.constraint] +end +``` + +You must implement one such pair of functions for each type of constraint the +bridge adds to the model. + +!!! warning + Avoid returning a list from the bridge object without copying it. Users + should be able to change the contents of the returned list without altering + the bridge object. + +For variables, the situation is simpler. If your bridge creates new variables, +you should implement the [`NumberOfVariables`](@ref) and +[`ListOfVariableIndices`](@ref) attributes. However, these attributes do not have +parameters, unlike their constraint counterparts. Only two functions suffice: + +```julia +function get( + ::SignBridge{T}, + ::NumberOfVariables, +) where {T} + return 0 +end + +function get( + ::SignBridge{T}, + ::ListOfVariableIndices, +) where {T} + return VariableIndex[] +end +``` + +### Model modifications + +To avoid copying the model when the user request to change a constraint, MOI +provides [`modify`](@ref). Bridges can also implement this API to allow +certain changes, such as coefficient changes. + +In our case, a modification of a coefficient in the original constraint +(i.e. replacing the value of the coefficient of a variable in the affine +function) should be transmitted to the constraint created by the bridge, +but with a sign change. + +```julia +function modify( + model::ModelLike, + bridge::SignBridge, + change::ScalarCoefficientChange, +) + modify( + model, + bridge.constraint, + ScalarCoefficientChange(change.variable, -change.new_coefficient), + ) + return +end +``` + +### Bridge deletion + +When a bridge is deleted, the constraints it added should be deleted too. + +```julia +function delete(model::ModelLike, bridge::SignBridge) + delete(model, bridge.constraint) + return +end +``` diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index dd2c7e80d5..a661af2507 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -8,6 +8,13 @@ struct MockVariableAttribute <: MOI.AbstractVariableAttribute end struct MockConstraintAttribute <: MOI.AbstractConstraintAttribute end # A mock optimizer used for testing. +""" + MockOptimizer + +`MockOptimizer` is a fake optimizer especially useful for testing. Its main +feature is that it can store the values that should be returned for each +attribute. +""" mutable struct MockOptimizer{MT<:MOI.ModelLike} <: MOI.AbstractOptimizer inner_model::MT # Flags