diff --git a/docs/src/apimanual.md b/docs/src/apimanual.md index 0bf04755d0..9626aeab17 100644 --- a/docs/src/apimanual.md +++ b/docs/src/apimanual.md @@ -4,6 +4,7 @@ DocTestSetup = quote using MathOptInterface const MOI = MathOptInterface end +DocTestFilters = [r"MathOptInterface|MOI"] ``` # Manual @@ -931,6 +932,31 @@ between `y_i` and the vector of scalar-valued quadratic functions. ### Automatic reformulation +#### Variable reformulation + +A variable is often created 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) rewritting rule. Given an optimizer +`optimizer` implementing constrained variables in [`Nonnegatives`](@ref) and, +the optimizer +```jldoctest; setup=:(optimizer = MOI.Utilities.Model{Float64}()) +bridged_optimizer = MOI.Bridges.Variable.Vectorize{Float64}(optimizer) +MOI.supports_constraint(bridged_optimizer, MOI.SingleVariable, 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 select the +appropriate bridges for unsupported constrained variables. + #### Constraint reformulation A constraint often possess different equivalent formulations, but a solver may only support one of them. @@ -938,13 +964,28 @@ It would be duplicate work to implement rewritting rules in every solver wrapper Constraint bridges provide a way to define a rewritting rule on top of the MOI interface which can be used by any optimizer. Some rules also implement constraint modifications and constraint primal and duals 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. -The `SplitInterval` is the bridge optimizer that applies the `SplitIntervalBridge` rewritting rule. -Given an optimizer `optimizer` implementing `ScalarAffineFunction`-in-`GreaterThan` and `ScalarAffineFunction`-in-`LessThan`, the optimizer -``` -bridgedoptimizer = SplitInterval(optimizer) +For example, the [`Bridges.Constraint.SplitIntervalBridge`](@ref) defines the +reformulation of a [`ScalarAffineFunction`](@ref)-in-[`Interval`](@ref) +constraint into a [`ScalarAffineFunction`](@ref)-in-[`GreaterThan`](@ref) and a +[`ScalarAffineFunction`](@ref)-in-[`LessThan`](@ref) constraint. +The `Bridges.Constraint.SplitInterval` is the bridge optimizer that applies the +[`Bridges.Constraint.SplitIntervalBridge`](@ref) rewritting rule. Given an +optimizer `optimizer` implementing [`ScalarAffineFunction`](@ref)-in-[`GreaterThan`](@ref) +and [`ScalarAffineFunction`](@ref)-in-[`LessThan`](@ref), 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) are mainly +used for testing bridges. It is recommended to rather use +[`Bridges.full_bridge_optimizer`](@ref) which automatically select the +appropriate constraint bridges for unsupported constraints. + ## Implementing a solver interface diff --git a/docs/src/apireference.md b/docs/src/apireference.md index d33f6d0332..eabe78fc31 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -74,7 +74,6 @@ AbstractSubmittable submit ``` - ## Model Interface ```@docs @@ -191,8 +190,8 @@ delete(::ModelLike, ::Index) [`add_variables`](@ref) while *constrained variables* are the variables created with [`add_constrained_variable`](@ref) or [`add_constrained_variables`](@ref). Adding constrained variables instead of -constraining free variables with [`add_constraint`](@ref) allows Variable -bridges to be used. +constraining free variables with [`add_constraint`](@ref) allows +[Variable bridges](@ref) to be used. Note further that free variables that are constrained with [`add_constraint`](@ref) may be copied by [`copy_to`](@ref) with [`add_constrained_variable`](@ref) or [`add_constrained_variables`](@ref) by the @@ -480,17 +479,219 @@ Utilities.@model ## Bridges -Bridges can be used for automatic reformulation of a certain constraint type into equivalent constraints. +Bridges can be used for automatic reformulation of constrained variables or +constraints into equivalent formulations using constrained variables and +constraints of different types. There are two important concepts to distinguish: +* [`Bridges.AbstractBridge`](@ref)s are recipes implementing a specific + reformulation. Bridges are not directly subtypes of + [`Bridges.AbstractBridge`](@ref), they are either + [`Bridges.Variable.AbstractBridge`](@ref) or + [`Bridges.Constraint.AbstractBridge`](@ref). +* [`Bridges.AbstractBridgeOptimizer`](@ref) is a layer that can be applied to + another [`ModelLike`](@ref) to apply the reformulation. The + [`Bridges.LazyBridgeOptimizer`](@ref) automatically chooses the appropriate + bridges to use when a constrained variable or constraint is not supported + by using the list of bridges that were added to it by + [`Bridges.add_bridge`](@ref). [`Bridges.full_bridge_optimizer`](@ref) wraps a + model in a [`Bridges.LazyBridgeOptimizer`](@ref) where all the bridges defined + in MOI are added. This is the recommended way to use bridges in the + [Testing guideline](@ref), and JuMP automatically calls this function when + attaching an optimizer. + ```@docs Bridges.AbstractBridge -Bridges.Constraint.AbstractBridge Bridges.AbstractBridgeOptimizer -Bridges.Constraint.SingleBridgeOptimizer Bridges.LazyBridgeOptimizer Bridges.add_bridge +Bridges.full_bridge_optimizer +``` + +### Variable bridges + +When variables are added, either free with +[`add_variable`](@ref)/[`add_variables`](@ref), +or constrained with +[`add_constrained_variable`](@ref)/[`add_constrained_variables`](@ref), +variable bridges allow to return *bridged variables* that do not correspond to +variables of the underlying model. These variables are parametrized by +variables of the underlying model and this parametrization can be obtained with +[`Bridges.bridged_variable_function`](@ref). Similarly, the variables of the +underlying model that were created by the bridge can be expressed in terms of +the bridged variables and this expression can be obtained with +[`Bridges.unbridged_variable_function`](@ref). +For instance, consider a model bridged by the +[`Bridges.Variable.VectorizeBridge`](@ref): +```jldoctest bridged_variable_function +model = MOI.Utilities.Model{Float64}() +bridged_model = MOI.Bridges.Variable.Vectorize{Float64}(model) +bridged_variable, bridged_constraint = MOI.add_constrained_variable(bridged_model, MOI.GreaterThan(1.0)) + +# output + +(MOI.VariableIndex(-1), MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}(-1)) +``` +The constrained variable in `MOI.GreaterThan(1.0)` returned is a bridged +variable as its index in negative. In `model`, a constrained variable in +`MOI.Nonnegatives` is created: +```jldoctest bridged_variable_function +inner_variables = MOI.get(model, MOI.ListOfVariableIndices()) + +# output + +1-element Array{MOI.VariableIndex,1}: + MOI.VariableIndex(1) +``` +In the functions used for adding constraints or setting the objective to +`bridged_model`, `bridged_variable` is substituted for `inner_variables[1]` plus +1: +```jldoctest bridged_variable_function +MOI.Bridges.bridged_variable_function(bridged_model, bridged_variable) + +# output + +MOI.ScalarAffineFunction{Float64}(MOI.ScalarAffineTerm{Float64}[ScalarAffineTerm{Float64}(1.0, VariableIndex(1))], 1.0) +``` +When getting [`ConstraintFunction`](@ref) or [`ObjectiveFunction`](@ref), +`inner_variables[1]` is substituted for `bridged_variable` minus 1: +```jldoctest bridged_variable_function +MOI.Bridges.unbridged_variable_function(bridged_model, inner_variables[1]) + +# output + +MOI.ScalarAffineFunction{Float64}(MOI.ScalarAffineTerm{Float64}[ScalarAffineTerm{Float64}(1.0, VariableIndex(-1))], -1.0) +``` + + +!!! note + A notable exception is with [`Bridges.Variable.ZerosBridge`](@ref) where no + variable is created in the underlying model as the variables are simply + transformed to zeros. When this bridge is used, it is not possible to + recover functions with bridged variables from functions of the inner + model. Consider for instance that we create two zero variables: + ```jldoctest cannot_unbridge_zero + model = MOI.Utilities.Model{Float64}() + bridged_model = MOI.Bridges.Variable.Zeros{Float64}(model) + bridged_variables, bridged_constraint = MOI.add_constrained_variables(bridged_model, MOI.Zeros(2)) + + # output + + (MOI.VariableIndex[VariableIndex(-1), VariableIndex(-2)], MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Zeros}(-1)) + ``` + Consider the following functions in the variables of `bridged_model`: + ```jldoctest cannot_unbridge_zero + func = MOI.Utilities.operate(+, Float64, MOI.SingleVariable.(bridged_variables)...) + + # output + + MOI.ScalarAffineFunction{Float64}(MOI.ScalarAffineTerm{Float64}[ScalarAffineTerm{Float64}(1.0, VariableIndex(-1)), ScalarAffineTerm{Float64}(1.0, VariableIndex(-2))], 0.0) + ``` + We can obtain the equivalent function in the variables of `model` as follows: + ```jldoctest cannot_unbridge_zero + inner_func = MOI.Bridges.bridged_function(bridged_model, func) + + # output + + MOI.ScalarAffineFunction{Float64}(MOI.ScalarAffineTerm{Float64}[], 0.0) + ``` + However, it's not possible to invert this operations. Indeed, since the + bridged variables are substituted for zeros, we cannot deduce whether + they were present in the initial function. + ```jldoctest cannot_unbridge_zero; filter = r"Stacktrace:.*"s + MOI.Bridges.unbridged_function(bridged_model, inner_func) + + # output + + ERROR: Cannot unbridge function because some variables are bridged by variable bridges that do not support reverse mapping, e.g., `ZerosBridge`. + Stacktrace: + [1] error(::String, ::String, ::String) at ./error.jl:42 + [2] throw_if_cannot_unbridge at /home/blegat/.julia/dev/MathOptInterface/src/Bridges/Variable/map.jl:343 [inlined] + [3] unbridged_function(::MOI.Bridges.Variable.SingleBridgeOptimizer{MOI.Bridges.Variable.ZerosBridge{Float64},MOI.Utilities.Model{Float64}}, ::MOI.ScalarAffineFunction{Float64}) at /home/blegat/.julia/dev/MOI/src/Bridges/bridge_optimizer.jl:920 + [4] top-level scope at none:0 + ``` + +```@docs +Bridges.Variable.AbstractBridge +Bridges.bridged_variable_function +Bridges.unbridged_variable_function ``` -Below is the list of bridges implemented in this package. +Below is the list of variable bridges implemented in this package. +```@docs +Bridges.Variable.ZerosBridge +Bridges.Variable.FreeBridge +Bridges.Variable.NonposToNonnegBridge +Bridges.Variable.VectorizeBridge +Bridges.Variable.RSOCtoPSDBridge +``` + +For each bridge defined in this package, a corresponding +[`Bridges.Variable.SingleBridgeOptimizer`](@ref) is available with the same +name without the "Bridge" suffix, e.g., `SplitInterval` is a +`SingleBridgeOptimizer` for the `SplitIntervalBridge`. Moreover, they are all +added in the [`Bridges.LazyBridgeOptimizer`](@ref) returned by +[`Bridges.full_bridge_optimizer`](@ref) as it calls +[`Bridges.Variable.add_all_bridges`](@ref). +```@docs +Bridges.Variable.SingleBridgeOptimizer +Bridges.Variable.add_all_bridges +``` + +### Constraint bridges + +When constraints are added with [`add_constraint`](@ref), constraint bridges +allow to return *bridged constraints* that do not correspond to +constraints of the underlying model. These constraints were enforced by an +equivalent formulation that added constraints (and possibly also variables) in +the underlying model. +For instance, consider a model bridged by the +[`Bridges.Constraint.SplitIntervalBridge`](@ref): +```jldoctest split_interval +model = MOI.Utilities.Model{Float64}() +bridged_model = MOI.Bridges.Constraint.SplitInterval{Float64}(model) +x, y = MOI.add_variables(bridged_model, 2) +func = MOI.Utilities.operate(+, Float64, MOI.SingleVariable(x), MOI.SingleVariable(y)) +c = MOI.add_constraint(bridged_model, func, MOI.Interval(1.0, 2.0)) + +# output + +MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},MOI.Interval{Float64}}(1) +``` +We can see the constraint was bridged to two constraints, one for each bound, +in the inner model. +```jldoctest split_interval +MOI.get(model, MOI.ListOfConstraints()) + +# output + +2-element Array{Tuple{DataType,DataType},1}: + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) +``` +However, `bridged_model` hides transparently hides these constraints and create the +illusion that an interval constraint was created. +```jldoctest split_interval +MOI.get(bridged_model, MOI.ListOfConstraints()) + +# output + +1-element Array{Tuple{DataType,DataType},1}: + (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) +``` +It is nevertheless possible to differentiate this constraint from a constraint +added to the inner model by asking whether it is bridged: +```jldoctest split_interval +MOI.Bridges.is_bridged(bridged_model, c) + +# output + +true +``` + +```@docs +Bridges.Constraint.AbstractBridge +``` + +Below is the list of constraint bridges implemented in this package. ```@docs Bridges.Constraint.GreaterToLessBridge Bridges.Constraint.LessToGreaterBridge @@ -513,20 +714,36 @@ Bridges.Constraint.SOCtoPSDBridge Bridges.Constraint.RSOCtoPSDBridge Bridges.Constraint.IndicatorActiveOnFalseBridge ``` -For each bridge defined in this package, a corresponding bridge optimizer is available with the same name without the "Bridge" suffix, e.g., `SplitInterval` is an `SingleBridgeOptimizer` for the `SplitIntervalBridge`. -Moreover, a `LazyBridgeOptimizer` with all the bridges defined in this package can be obtained with +For each bridge defined in this package, a corresponding +[`Bridges.Constraint.SingleBridgeOptimizer`](@ref) is available with the same +name without the "Bridge" suffix, e.g., `SplitInterval` is a +`SingleBridgeOptimizer` for the `SplitIntervalBridge`. Moreover, they are all +added in the [`Bridges.LazyBridgeOptimizer`](@ref) returned by +[`Bridges.full_bridge_optimizer`](@ref) as it calls +[`Bridges.Constraint.add_all_bridges`](@ref). ```@docs -Bridges.full_bridge_optimizer +Bridges.Constraint.SingleBridgeOptimizer +Bridges.Constraint.add_all_bridges ``` ### Bridge interface A bridge should implement the following functions to be usable by a bridge optimizer: ```@docs +Bridges.added_constrained_variable_types +Bridges.added_constraint_types +``` +Additionally, variable bridges should implement: +```@docs +Bridges.Variable.supports_constrained_variable +Bridges.Variable.concrete_bridge_type +Bridges.Variable.bridge_constrained_variable +``` +and constraint bridges should implement +```@docs supports_constraint(::Type{<:Bridges.Constraint.AbstractBridge}, ::Type{<:AbstractFunction}, ::Type{<:AbstractSet}) Bridges.Constraint.concrete_bridge_type Bridges.Constraint.bridge_constraint -Bridges.added_constraint_types ``` When querying the [`NumberOfVariables`](@ref), [`NumberOfConstraints`](@ref) @@ -537,8 +754,9 @@ constraints it has creates by implemented the following methods of [`get`](@ref): ```@docs get(::Bridges.Constraint.AbstractBridge, ::NumberOfVariables) -get(::Bridges.Constraint.AbstractBridge, ::NumberOfConstraints) -get(::Bridges.Constraint.AbstractBridge, ::ListOfConstraintIndices) +get(::Bridges.Constraint.AbstractBridge, ::ListOfVariableIndices) +get(::Bridges.AbstractBridge, ::NumberOfConstraints) +get(::Bridges.AbstractBridge, ::ListOfConstraintIndices) ``` ## Copy utilities @@ -644,7 +862,7 @@ is set to `AUTOMATIC` or to `MANUAL`. a constraint) results in a drop to the state `EMPTY_OPTIMIZER`. When calling [`Utilities.attach_optimizer`](@ref), the `CachingOptimizer` copies -the cached model to the optimizer with [`MathOptInterface.copy_to`](@ref). +the cached model to the optimizer with [`copy_to`](@ref). We refer to [Implementing copy](@ref) for more details. ```@docs diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 8aa142e286..dc7f181d77 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -22,28 +22,10 @@ include("lazy_bridge_optimizer.jl") Returns a `LazyBridgeOptimizer` bridging `model` for every bridge defined in this package and for the coefficient type `T`. """ -function full_bridge_optimizer(model::MOI.ModelLike, ::Type{T}) where T +function full_bridge_optimizer(model::MOI.ModelLike, T::Type) bridged_model = LazyBridgeOptimizer(model) - add_bridge(bridged_model, Constraint.GreaterToLessBridge{T}) - add_bridge(bridged_model, Constraint.LessToGreaterBridge{T}) - add_bridge(bridged_model, Constraint.NonnegToNonposBridge{T}) - add_bridge(bridged_model, Constraint.NonposToNonnegBridge{T}) - add_bridge(bridged_model, Constraint.ScalarizeBridge{T}) - add_bridge(bridged_model, Constraint.VectorizeBridge{T}) - add_bridge(bridged_model, Constraint.ScalarSlackBridge{T}) - add_bridge(bridged_model, Constraint.VectorSlackBridge{T}) - add_bridge(bridged_model, Constraint.ScalarFunctionizeBridge{T}) - add_bridge(bridged_model, Constraint.VectorFunctionizeBridge{T}) - add_bridge(bridged_model, Constraint.SplitIntervalBridge{T}) - add_bridge(bridged_model, Constraint.QuadtoSOCBridge{T}) - add_bridge(bridged_model, Constraint.GeoMeanBridge{T}) - add_bridge(bridged_model, Constraint.SquareBridge{T}) - add_bridge(bridged_model, Constraint.LogDetBridge{T}) - add_bridge(bridged_model, Constraint.RootDetBridge{T}) - add_bridge(bridged_model, Constraint.RSOCBridge{T}) - add_bridge(bridged_model, Constraint.RSOCtoPSDBridge{T}) - add_bridge(bridged_model, Constraint.SOCtoPSDBridge{T}) - add_bridge(bridged_model, Constraint.IndicatorActiveOnFalseBridge{T}) + Variable.add_all_bridges(bridged_model, T) + Constraint.add_all_bridges(bridged_model, T) return bridged_model end diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index ce21266c83..5041121052 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -50,4 +50,34 @@ const SOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SOCtoPSDBridge{T}, const RSOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoPSDBridge{T}, OT} include("indicator.jl") +""" + add_all_bridges(bridged_model, T::Type) + +Add all bridges defined in the `Bridges.Constraint` submodule to +`bridged_model`. The coefficient type used is `T`. +""" +function add_all_bridges(bridged_model, T::Type) + MOIB.add_bridge(bridged_model, GreaterToLessBridge{T}) + MOIB.add_bridge(bridged_model, LessToGreaterBridge{T}) + MOIB.add_bridge(bridged_model, NonnegToNonposBridge{T}) + MOIB.add_bridge(bridged_model, NonposToNonnegBridge{T}) + MOIB.add_bridge(bridged_model, ScalarizeBridge{T}) + MOIB.add_bridge(bridged_model, VectorizeBridge{T}) + MOIB.add_bridge(bridged_model, ScalarSlackBridge{T}) + MOIB.add_bridge(bridged_model, VectorSlackBridge{T}) + MOIB.add_bridge(bridged_model, ScalarFunctionizeBridge{T}) + MOIB.add_bridge(bridged_model, VectorFunctionizeBridge{T}) + MOIB.add_bridge(bridged_model, SplitIntervalBridge{T}) + MOIB.add_bridge(bridged_model, QuadtoSOCBridge{T}) + MOIB.add_bridge(bridged_model, GeoMeanBridge{T}) + MOIB.add_bridge(bridged_model, SquareBridge{T}) + MOIB.add_bridge(bridged_model, LogDetBridge{T}) + MOIB.add_bridge(bridged_model, RootDetBridge{T}) + MOIB.add_bridge(bridged_model, RSOCBridge{T}) + MOIB.add_bridge(bridged_model, RSOCtoPSDBridge{T}) + MOIB.add_bridge(bridged_model, SOCtoPSDBridge{T}) + MOIB.add_bridge(bridged_model, IndicatorActiveOnFalseBridge{T}) + return +end + end diff --git a/src/Bridges/Constraint/slack.jl b/src/Bridges/Constraint/slack.jl index 0d3dacb7e2..e639a7b211 100644 --- a/src/Bridges/Constraint/slack.jl +++ b/src/Bridges/Constraint/slack.jl @@ -15,9 +15,8 @@ struct ScalarSlackBridge{T, F, S} <: AbstractBridge end function bridge_constraint(::Type{ScalarSlackBridge{T, F, S}}, model, f::MOI.AbstractScalarFunction, s::S) where {T, F, S} - slack = MOI.add_variable(model) + slack, slack_in_set = MOI.add_constrained_variable(model, s) new_f = MOIU.operate(-, T, f, MOI.SingleVariable(slack)) - slack_in_set = MOI.add_constraint(model, MOI.SingleVariable(slack), s) equality = MOI.add_constraint(model, new_f, MOI.EqualTo(0.0)) return ScalarSlackBridge{T, F, S}(slack, slack_in_set, equality) end @@ -36,9 +35,11 @@ MOI.supports_constraint(::Type{ScalarSlackBridge{T}}, MOI.supports_constraint(::Type{ScalarSlackBridge{T}}, ::Type{<:MOI.AbstractScalarFunction}, ::Type{<:MOI.EqualTo}) where {T} = false -MOIB.added_constrained_variable_types(::Type{<:ScalarSlackBridge}) = Tuple{DataType}[] +function MOIB.added_constrained_variable_types(::Type{<:ScalarSlackBridge{T, F, S}}) where {T, F, S} + return [(S,)] +end function MOIB.added_constraint_types(::Type{ScalarSlackBridge{T, F, S}}) where {T, F, S} - return [(F, MOI.EqualTo{T}), (MOI.SingleVariable, S)] + return [(F, MOI.EqualTo{T})] end function concrete_bridge_type(::Type{<:ScalarSlackBridge{T}}, F::Type{<:MOI.AbstractScalarFunction}, @@ -48,16 +49,16 @@ function concrete_bridge_type(::Type{<:ScalarSlackBridge{T}}, end # Attributes, Bridge acting as a model -MOI.get(b::ScalarSlackBridge{T, F, S}, ::MOI.NumberOfVariables) where {T, F, S} = 1 -MOI.get(b::ScalarSlackBridge{T, F, S}, ::MOI.NumberOfConstraints{F, MOI.EqualTo{T}}) where {T, F, S} = 1 +MOI.get(b::ScalarSlackBridge, ::MOI.NumberOfVariables) = 1 +MOI.get(b::ScalarSlackBridge, ::MOI.ListOfVariableIndices) = [b.slack] +MOI.get(b::ScalarSlackBridge{T, F}, ::MOI.NumberOfConstraints{F, MOI.EqualTo{T}}) where {T, F} = 1 MOI.get(b::ScalarSlackBridge{T, F, S}, ::MOI.NumberOfConstraints{MOI.SingleVariable, S}) where {T, F, S} = 1 -MOI.get(b::ScalarSlackBridge{T, F, S}, ::MOI.ListOfConstraintIndices{F, MOI.EqualTo{T}}) where {T, F, S} = [b.equality] +MOI.get(b::ScalarSlackBridge{T, F}, ::MOI.ListOfConstraintIndices{F, MOI.EqualTo{T}}) where {T, F} = [b.equality] MOI.get(b::ScalarSlackBridge{T, F, S}, ::MOI.ListOfConstraintIndices{MOI.SingleVariable, S}) where {T, F, S} = [b.slack_in_set] # Indices function MOI.delete(model::MOI.ModelLike, c::ScalarSlackBridge) MOI.delete(model, c.equality) - MOI.delete(model, c.slack_in_set) MOI.delete(model, c.slack) return end @@ -119,9 +120,8 @@ end function bridge_constraint(::Type{VectorSlackBridge{T, F, S}}, model, f::MOI.AbstractVectorFunction, s::S) where {T, F, S} d = MOI.dimension(s) - slacks = MOI.add_variables(model, d) + slacks, slacks_in_set = MOI.add_constrained_variables(model, s) new_f = MOIU.operate(-, T, f, MOI.VectorOfVariables(slacks)) - slacks_in_set = MOI.add_constraint(model, MOI.VectorOfVariables(slacks), s) equality = MOI.add_constraint(model, new_f, MOI.Zeros(d)) return VectorSlackBridge{T, F, S}(slacks, slacks_in_set, equality) end @@ -138,9 +138,11 @@ MOI.supports_constraint(::Type{VectorSlackBridge{T}}, MOI.supports_constraint(::Type{VectorSlackBridge{T}}, ::Type{<:MOI.VectorOfVariables}, ::Type{<:MOI.AbstractVectorSet}) where {T} = false -MOIB.added_constrained_variable_types(::Type{<:VectorSlackBridge}) = Tuple{DataType}[] +function MOIB.added_constrained_variable_types(::Type{<:VectorSlackBridge{T, F, S}}) where {T, F, S} + return [(S,)] +end function MOIB.added_constraint_types(::Type{VectorSlackBridge{T, F, S}}) where {T, F<:MOI.AbstractVectorFunction, S} - return [(F, MOI.Zeros), (MOI.VectorOfVariables, S)] + return [(F, MOI.Zeros)] end function concrete_bridge_type(::Type{<:VectorSlackBridge{T}}, F::Type{<:MOI.AbstractVectorFunction}, @@ -150,16 +152,16 @@ function concrete_bridge_type(::Type{<:VectorSlackBridge{T}}, end # Attributes, Bridge acting as a model -MOI.get(b::VectorSlackBridge{T, F, S}, ::MOI.NumberOfVariables) where {T, F, S} = length(b.slacks) -MOI.get(b::VectorSlackBridge{T, F, S}, ::MOI.NumberOfConstraints{F, MOI.Zeros}) where {T, F, S} = 1 +MOI.get(b::VectorSlackBridge, ::MOI.NumberOfVariables) = length(b.slacks) +MOI.get(b::VectorSlackBridge, ::MOI.ListOfVariableIndices) = b.slacks +MOI.get(b::VectorSlackBridge{T, F}, ::MOI.NumberOfConstraints{F, MOI.Zeros}) where {T, F} = 1 MOI.get(b::VectorSlackBridge{T, F, S}, ::MOI.NumberOfConstraints{MOI.VectorOfVariables, S}) where {T, F, S} = 1 -MOI.get(b::VectorSlackBridge{T, F, S}, ::MOI.ListOfConstraintIndices{F, MOI.Zeros}) where {T, F, S} = [b.equality] +MOI.get(b::VectorSlackBridge{T, F}, ::MOI.ListOfConstraintIndices{F, MOI.Zeros}) where {T, F} = [b.equality] MOI.get(b::VectorSlackBridge{T, F, S}, ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S}) where {T, F, S} = [b.slacks_in_set] # Indices function MOI.delete(model::MOI.ModelLike, c::VectorSlackBridge) MOI.delete(model, c.equality) - MOI.delete(model, c.slacks_in_set) MOI.delete(model, c.slacks) return end diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index 3f8e68e404..70257205bb 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -14,4 +14,31 @@ include("map.jl") # Bridge optimizer bridging a specific variable bridge include("single_bridge_optimizer.jl") +# Variable bridges +include("zeros.jl") +const Zeros{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{ZerosBridge{T}, OT} +include("free.jl") +const Free{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{FreeBridge{T}, OT} +include("flip_sign.jl") +const NonposToNonneg{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{NonposToNonnegBridge{T}, OT} +include("vectorize.jl") +const Vectorize{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{VectorizeBridge{T}, OT} +include("rsoc_to_psd.jl") +const RSOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoPSDBridge{T}, OT} + +""" + add_all_bridges(bridged_model, T::Type) + +Add all bridges defined in the `Bridges.Variable` submodule to `bridged_model`. +The coefficient type used is `T`. +""" +function add_all_bridges(bridged_model, T::Type) + MOIB.add_bridge(bridged_model, ZerosBridge{T}) + MOIB.add_bridge(bridged_model, FreeBridge{T}) + MOIB.add_bridge(bridged_model, NonposToNonnegBridge{T}) + MOIB.add_bridge(bridged_model, VectorizeBridge{T}) + MOIB.add_bridge(bridged_model, RSOCtoPSDBridge{T}) + return +end + end diff --git a/src/Bridges/Variable/flip_sign.jl b/src/Bridges/Variable/flip_sign.jl new file mode 100644 index 0000000000..8eaada975a --- /dev/null +++ b/src/Bridges/Variable/flip_sign.jl @@ -0,0 +1,95 @@ +""" + FlipSignBridge{T, S1, S2} + +Bridge constrained variables in `S1` into constrained variables in `S2` by +multiplying the variables by `-1` and taking the point reflection of the set +across the origin. The flipped `MOI.VectorOfVariables`-in-`S` constraint is +stored in the `flipped_constraint` field by convention. +""" +abstract type FlipSignBridge{ + T, S1<:MOI.AbstractSet, S2<:MOI.AbstractSet} <: AbstractBridge end + +function supports_constrained_variable( + ::Type{<:FlipSignBridge{T, S1}}, ::Type{S1}) where {T, S1<:MOI.AbstractVectorSet} + return true +end +function MOIB.added_constrained_variable_types( + ::Type{<:FlipSignBridge{T, S1, S2}}) where {T, S1, S2} + return [(S2,)] +end +function MOIB.added_constraint_types(::Type{<:FlipSignBridge}) + return Tuple{DataType, DataType}[] +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::FlipSignBridge, ::MOI.NumberOfVariables) + return length(bridge.flipped_variables) +end +function MOI.get(bridge::FlipSignBridge, ::MOI.ListOfVariableIndices) + return bridge.flipped_variables +end +function MOI.get(::FlipSignBridge{T, S1, S2}, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, S2}) where {T, S1, S2<:MOI.AbstractVectorSet} + return 1 +end +function MOI.get(bridge::FlipSignBridge{T, S1, S2}, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S2}) where {T, S1, S2<:MOI.AbstractVectorSet} + return [bridge.flipped_constraint] +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::FlipSignBridge) + MOI.delete(model, bridge.flipped_variables) +end + +function MOI.delete(model::MOI.ModelLike, bridge::FlipSignBridge, i::IndexInVector) + MOI.delete(model, bridge.flipped_variables[i.value]) + deleteat!(bridge.flipped_variables, i.value) +end + +# Attributes, Bridge acting as a constraint + +function MOI.get(model::MOI.ModelLike, + attr::Union{MOI.ConstraintPrimal, MOI.ConstraintDual}, + bridge::FlipSignBridge) + return -MOI.get(model, attr, bridge.flipped_constraint) +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::FlipSignBridge, i::IndexInVector) + return -MOI.get(model, attr, bridge.flipped_variables[i.value]) +end + +function MOIB.bridged_function(bridge::FlipSignBridge{T}, i::IndexInVector) where T + func = MOI.SingleVariable(bridge.flipped_variables[i.value]) + return MOIU.operate(-, T, func) +end +function unbridged_map(bridge::FlipSignBridge{T}, vi::MOI.VariableIndex, + i::IndexInVector) where T + func = MOIU.operate(-, T, MOI.SingleVariable(vi)) + return (bridge.flipped_variables[i.value] => func,) +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart, + bridge::FlipSignBridge, value, i::IndexInVector) + MOI.set(model, attr, bridge.flipped_variables[i.value], -value) +end + +""" + NonposToNonnegBridge{T, F<:MOI.AbstractVectorFunction, G<:MOI.AbstractVectorFunction} <: + FlipSignBridge{T, MOI.Nonpositives, MOI.Nonnegatives, F, G} + +Transforms constrained variables in `Nonpositives` into constrained variables in +`Nonnegatives`. +""" +struct NonposToNonnegBridge{T} <: FlipSignBridge{T, MOI.Nonpositives, MOI.Nonnegatives} + flipped_variables::Vector{MOI.VariableIndex} + flipped_constraint::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Nonnegatives} +end +function bridge_constrained_variable(::Type{NonposToNonnegBridge{T}}, + model::MOI.ModelLike, + set::MOI.Nonpositives) where T + flipped_variables, flipped_constraint = MOI.add_constrained_variables( + model, MOI.Nonnegatives(set.dimension)) + return NonposToNonnegBridge{T}(flipped_variables, flipped_constraint) +end diff --git a/src/Bridges/Variable/free.jl b/src/Bridges/Variable/free.jl new file mode 100644 index 0000000000..46e28e707e --- /dev/null +++ b/src/Bridges/Variable/free.jl @@ -0,0 +1,127 @@ +""" + FreeBridge{T} <: Bridges.Variable.AbstractBridge + +Transforms constrained variables in [`MathOptInterface.Reals`](@ref) +to the sum of constrained variables in [`MathOptInterface.Nonnegatives`](@ref) +and constrained variables in [`MathOptInterface.Nonpositives`](@ref). +""" +struct FreeBridge{T} <: AbstractBridge + nonneg_variables::Vector{MOI.VariableIndex} + nonneg_constraint::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Nonnegatives} + nonpos_variables::Vector{MOI.VariableIndex} + nonpos_constraint::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Nonpositives} +end +function bridge_constrained_variable(::Type{FreeBridge{T}}, + model::MOI.ModelLike, + set::MOI.Reals) where T + nonneg_variables, nonneg_constraint = MOI.add_constrained_variables( + model, MOI.Nonnegatives(MOI.dimension(set))) + nonpos_variables, nonpos_constraint = MOI.add_constrained_variables( + model, MOI.Nonpositives(MOI.dimension(set))) + return FreeBridge{T}(nonneg_variables, nonneg_constraint, + nonpos_variables, nonpos_constraint) +end + +function supports_constrained_variable( + ::Type{<:FreeBridge}, ::Type{MOI.Reals}) + return true +end +function MOIB.added_constrained_variable_types(::Type{<:FreeBridge}) + return [(MOI.Nonnegatives,), (MOI.Nonpositives,)] +end +function MOIB.added_constraint_types(::Type{FreeBridge{T}}) where T + return Tuple{DataType, DataType}[] +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::FreeBridge, ::MOI.NumberOfVariables) + return length(bridge.nonneg_variables) + length(bridge.nonpos_variables) +end +function MOI.get(bridge::FreeBridge, ::MOI.ListOfVariableIndices) + return vcat(bridge.nonneg_variables, bridge.nonpos_variables) +end +function MOI.get(bridge::FreeBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, + MOI.Nonnegatives}) + return 1 +end +function MOI.get(bridge::FreeBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, + MOI.Nonnegatives}) + return [bridge.nonneg_constraint] +end +function MOI.get(bridge::FreeBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, + MOI.Nonpositives}) + return 1 +end +function MOI.get(bridge::FreeBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, + MOI.Nonpositives}) + return [bridge.nonpos_constraint] +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::FreeBridge) + MOI.delete(model, bridge.nonneg_variables) + MOI.delete(model, bridge.nonpos_variables) +end + +function MOI.delete(model::MOI.ModelLike, bridge::FreeBridge, i::IndexInVector) + MOI.delete(model, bridge.nonneg_variables[i.value]) + deleteat!(bridge.nonneg_variables, i.value) + MOI.delete(model, bridge.nonpos_variables[i.value]) + deleteat!(bridge.nonpos_variables, i.value) +end + + +# Attributes, Bridge acting as a constraint + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::FreeBridge{T}) where T + return MOI.get(model, attr, bridge.nonneg_constraint) + + MOI.get(model, attr, bridge.nonpos_constraint) +end +# The transformation is x_free = [I I] * [x_nonneg; x_nonpos] +# so the transformation of the dual is +# [y_nonneg; y_nonpos] = [I; I] * y_free +# that is +# y_nonneg = y_nonpos = y_free +# We can therefore take either of them, let's take y_nonneg. +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::FreeBridge{T}) where T + return MOI.get(model, attr, bridge.nonneg_constraint) +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::FreeBridge{T}, i::IndexInVector) where T + return MOI.get(model, attr, bridge.nonneg_variables[i.value]) + + MOI.get(model, attr, bridge.nonpos_variables[i.value]) +end + +function MOIB.bridged_function(bridge::FreeBridge{T}, i::IndexInVector) where T + return MOIU.operate(+, T, MOI.SingleVariable(bridge.nonneg_variables[i.value]), + MOI.SingleVariable(bridge.nonpos_variables[i.value])) +end +# x_free has been replaced by x_nonneg + x_nonpos. +# To undo it we replace x_nonneg by x_free and x_nonpos by 0. +function unbridged_map(bridge::FreeBridge{T}, vi::MOI.VariableIndex, + i::IndexInVector) where T + sv = MOI.SingleVariable(vi) + func = convert(MOI.ScalarAffineFunction{T}, sv) + return bridge.nonneg_variables[i.value] => func, + bridge.nonpos_variables[i.value] => zero(MOI.ScalarAffineFunction{T}) +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart, + bridge::FreeBridge, value, i::IndexInVector) + if value < 0 + nonneg = zero(value) + nonpos = value + else + nonneg = value + nonpos = zero(value) + end + MOI.set(model, attr, bridge.nonneg_variables[i.value], nonneg) + MOI.set(model, attr, bridge.nonpos_variables[i.value], nonpos) +end diff --git a/src/Bridges/Variable/rsoc_to_psd.jl b/src/Bridges/Variable/rsoc_to_psd.jl new file mode 100644 index 0000000000..a8aa738993 --- /dev/null +++ b/src/Bridges/Variable/rsoc_to_psd.jl @@ -0,0 +1,174 @@ +""" + RSOCtoPSDBridge{T} <: Bridges.Variable.AbstractBridge + +Transforms constrained variables in [`MathOptInterface.RotatedSecondOrderCone`](@ref) +to constrained variables in [`MathOptInterface.PositiveSemidefiniteConeTriangle`](@ref). +""" +struct RSOCtoPSDBridge{T} <: AbstractBridge + # `t` is `variables[1]` + # `u` is `variables[2]/2` + # `x` is `variables[[3, 5, 8, ...]]` + variables::Vector{MOI.VariableIndex} + psd::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle} + off_diag::Vector{MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{T}}} + diag::Vector{MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}} +end +function bridge_constrained_variable(::Type{RSOCtoPSDBridge{T}}, + model::MOI.ModelLike, + set::MOI.RotatedSecondOrderCone) where T + dim = set.dimension - 1 + variables, psd = MOI.add_constrained_variables( + model, MOI.PositiveSemidefiniteConeTriangle(dim)) + # This is `2 * u` + u2 = MOI.SingleVariable(variables[3]) + off_diag = MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{T}}[] + diag = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}[] + k = 3 + for j in 3:dim + k += 1 + for i in 2:(j-1) + k += 1 + push!(off_diag, + MOI.add_constraint(model, MOI.SingleVariable(variables[k]), + MOI.EqualTo(zero(T)))) + end + k += 1 + func = MOIU.operate(-, T, u2, MOI.SingleVariable(variables[k])) + push!(diag, MOI.add_constraint(model, func, MOI.EqualTo(zero(T)))) + end + @assert k == trimap(dim, dim) + return RSOCtoPSDBridge{T}(variables, psd, off_diag, diag) +end + +function supports_constrained_variable( + ::Type{<:RSOCtoPSDBridge}, ::Type{MOI.RotatedSecondOrderCone}) + return true +end +function MOIB.added_constrained_variable_types(::Type{<:RSOCtoPSDBridge}) + return [(MOI.PositiveSemidefiniteConeTriangle,)] +end +function MOIB.added_constraint_types(::Type{RSOCtoPSDBridge{T}}) where T + return [(MOI.SingleVariable, MOI.EqualTo{T}), (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T})] +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::RSOCtoPSDBridge, ::MOI.NumberOfVariables) + return length(bridge.variables) +end +function MOI.get(bridge::RSOCtoPSDBridge, ::MOI.ListOfVariableIndices) + return bridge.variables +end +function MOI.get(bridge::RSOCtoPSDBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, + MOI.PositiveSemidefiniteConeTriangle}) + return 1 +end +function MOI.get(bridge::RSOCtoPSDBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, + MOI.PositiveSemidefiniteConeTriangle}) + return [bridge.psd] +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.NumberOfConstraints{MOI.SingleVariable, + MOI.EqualTo{T}}) where T + return length(bridge.off_diag) +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.SingleVariable, + MOI.EqualTo{T}}) where T + return bridge.off_diag +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}}) where T + return length(bridge.diag) +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}}) where T + return bridge.diag +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::RSOCtoPSDBridge) + for ci in bridge.diag + MOI.delete(model, ci) + end + MOI.delete(model, bridge.variables) +end + +# Attributes, Bridge acting as a constraint + +function trimap(i::Integer, j::Integer) + if i < j + trimap(j, i) + else + div((i-1)*i, 2) + j + end +end +function _variable_map(i::IndexInVector) + if i.value == 1 + return 1 + elseif i.value == 2 + return 3 + else + return trimap(1, i.value - 1) + end +end +function _variable(bridge::RSOCtoPSDBridge, i::IndexInVector) + return bridge.variables[_variable_map(i)] +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::RSOCtoPSDBridge{T}) where T + values = MOI.get(model, attr, bridge.psd) + n = length(bridge.diag) + 3 + mapped = [values[_variable_map(IndexInVector(i))] for i in 1:n] + mapped[2] /= 2 + return mapped +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::RSOCtoPSDBridge{T}) where T + dual = MOI.get(model, attr, bridge.psd) + n = length(bridge.diag) + 3 + mapped = [dual[_variable_map(IndexInVector(i))] for i in 1:n] + for ci in bridge.diag + mapped[2] += MOI.get(model, attr, ci) + end + for i in 2:length(mapped) + # For `i = 2`, we multiply by 2 because it is `2u`. + # For `i > 2`, we multiply by 2 because to account for the difference + # of scalar product `MOIU.set_dot`. + mapped[i] *= 2 + end + return mapped +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::RSOCtoPSDBridge{T}, i::IndexInVector) where T + value = MOI.get(model, attr, _variable(bridge, i)) + if i.value == 2 + return value / 2 + else + return value + end +end + +function MOIB.bridged_function(bridge::RSOCtoPSDBridge{T}, i::IndexInVector) where T + func = MOI.SingleVariable(_variable(bridge, i)) + if i.value == 2 + return MOIU.operate(/, T, func, convert(T, 2)) + else + return convert(MOI.ScalarAffineFunction{T}, func) + end +end +function unbridged_map(bridge::RSOCtoPSDBridge{T}, vi::MOI.VariableIndex, + i::IndexInVector) where T + sv = MOI.SingleVariable(vi) + if i.value == 2 + func = MOIU.operate(*, T, convert(T, 2), sv) + else + func = convert(MOI.ScalarAffineFunction{T}, sv) + end + return (_variable(bridge, i) => func,) +end diff --git a/src/Bridges/Variable/vectorize.jl b/src/Bridges/Variable/vectorize.jl new file mode 100644 index 0000000000..8f684d5b2d --- /dev/null +++ b/src/Bridges/Variable/vectorize.jl @@ -0,0 +1,110 @@ +""" + VectorizeBridge{T, S} + +Transforms a constrained variable in `scalar_set_type(S, T)` where +`S <: VectorLinearSet` constrained variables in `S`. +""" +mutable struct VectorizeBridge{T, S} <: AbstractBridge + variable::MOI.VariableIndex + vector_constraint::MOI.ConstraintIndex{MOI.VectorOfVariables, S} + set_constant::T # constant in scalar set +end +function bridge_constrained_variable( + ::Type{VectorizeBridge{T, S}}, + model::MOI.ModelLike, set::MOIU.ScalarLinearSet{T}) where {T, S} + set_constant = MOI.constant(set) + variables, vector_constraint = MOI.add_constrained_variables(model, S(1)) + return VectorizeBridge{T, S}(variables[1], vector_constraint, set_constant) +end + +function supports_constrained_variable( + ::Type{VectorizeBridge{T}}, ::Type{<:MOIU.ScalarLinearSet{T}}) where T + return true +end +function MOIB.added_constrained_variable_types(::Type{VectorizeBridge{T, S}}) where {T, S} + return [(S,)] +end +function MOIB.added_constraint_types(::Type{<:VectorizeBridge}) + return Tuple{DataType, DataType}[] +end +function concrete_bridge_type(::Type{<:VectorizeBridge{T}}, + S::Type{<:MOIU.ScalarLinearSet{T}}) where T + return VectorizeBridge{T, MOIU.vector_set_type(S)} +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::VectorizeBridge, ::MOI.NumberOfVariables) + return 1 +end +function MOI.get(bridge::VectorizeBridge, ::MOI.ListOfVariableIndices) + return [bridge.variable] +end +function MOI.get(::VectorizeBridge{T, S}, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, S}) where {T, S} + return 1 +end +function MOI.get(bridge::VectorizeBridge{T, S}, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S}) where {T, S} + return [bridge.vector_constraint] +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::VectorizeBridge) + MOI.delete(model, bridge.variable) +end + +# Attributes, Bridge acting as a constraint + +function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintSet, + bridge::VectorizeBridge{T, S}) where {T, S} + return MOIU.scalar_set_type(S, T)(bridge.set_constant) +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet, + bridge::VectorizeBridge, new_set::MOIU.ScalarLinearSet) + # This would require modifing any constraint which uses the bridged + # variable. + throw(MOI.SetAttributeNotAllowed(attr, + "The variable `$(bridge.variable)` is bridged by the `VectorizeBridge`.")) +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::VectorizeBridge) + x = MOI.get(model, attr, bridge.vector_constraint) + @assert length(x) == 1 + y = x[1] + if !MOIU.is_ray(MOI.get(model, MOI.PrimalStatus(attr.N))) + # If it is an infeasibility certificate, it is a ray and satisfies the + # homogenized problem, see https://github.com/JuliaOpt/MathOptInterface.jl/issues/433 + # Otherwise, we need to add the set constant since the ConstraintPrimal + # is defined as the value of the function and the set_constant was + # removed from the original function + y += bridge.set_constant + end + return y +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::VectorizeBridge) + x = MOI.get(model, attr, bridge.vector_constraint) + @assert length(x) == 1 + return x[1] +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::VectorizeBridge) + value = MOI.get(model, attr, bridge.variable) + if !MOIU.is_ray(MOI.get(model, MOI.PrimalStatus(attr.N))) + value += bridge.set_constant + end + return value +end + +function MOIB.bridged_function(bridge::VectorizeBridge{T}) where T + func = MOI.SingleVariable(bridge.variable) + return MOIU.operate(+, T, func, bridge.set_constant) +end +function unbridged_map(bridge::VectorizeBridge{T}, vi::MOI.VariableIndex) where T + func = MOIU.operate(-, T, MOI.SingleVariable(vi), + bridge.set_constant) + return (bridge.variable => func,) +end diff --git a/src/Bridges/Variable/zeros.jl b/src/Bridges/Variable/zeros.jl new file mode 100644 index 0000000000..663a58f9be --- /dev/null +++ b/src/Bridges/Variable/zeros.jl @@ -0,0 +1,63 @@ +""" + ZerosBridge{T} <: Bridges.Variable.AbstractBridge + +Transforms constrained variables in [`MathOptInterface.Zeros`](@ref) to zeros, +which ends up creating no variables in the underlying model. +The bridged variables are therefore similar to parameters with zero values. +Parameters with non-zero value can be created with constrained variables in +[`MOI.EqualTo`](@ref) by combining a [`VectorizeBridge`](@ref) and this bridge. +The functions cannot be unbridged, given a function, we cannot determine, if +the bridged variables were use. +The dual values cannot be determined by the bridge but they can be determined +by the bridged optimizer using [`MathOptInterface.Utilities.get_fallback`](@ref) +if a `CachingOptimizer` is used (since `ConstraintFunction` cannot be got +as functions cannot be unbridged). +""" +struct ZerosBridge{T} <: AbstractBridge + n::Int # Number of variables +end +function bridge_constrained_variable(::Type{ZerosBridge{T}}, + model::MOI.ModelLike, + set::MOI.Zeros) where T + return ZerosBridge{T}(MOI.dimension(set)) +end + +function supports_constrained_variable( + ::Type{<:ZerosBridge}, ::Type{MOI.Zeros}) + return true +end +function MOIB.added_constrained_variable_types(::Type{<:ZerosBridge}) + return Tuple{DataType}[] +end +function MOIB.added_constraint_types(::Type{<:ZerosBridge}) + return Tuple{DataType, DataType}[] +end + +# Attributes, Bridge acting as a model +MOI.get(bridge::ZerosBridge, ::MOI.NumberOfVariables) = 0 +function MOI.get(bridge::ZerosBridge, ::MOI.ListOfVariableIndices) + return MOI.VariableIndex[] +end + +# References +function MOI.delete(::MOI.ModelLike, ::ZerosBridge) end + +# Attributes, Bridge acting as a constraint + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintPrimal, + bridge::ZerosBridge{T}) where T + return zeros(T, bridge.n) +end + +function MOI.get(::MOI.ModelLike, ::MOI.VariablePrimal, + ::ZerosBridge{T}, ::IndexInVector) where T + return zero(T) +end + +function MOIB.bridged_function(::ZerosBridge{T}, ::IndexInVector) where T + return zero(MOI.ScalarAffineFunction{T}) +end +function unbridged_map(::ZerosBridge, ::MOI.VariableIndex, + ::IndexInVector) + return nothing +end diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 493314b3f3..cfaf8216a5 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -931,6 +931,13 @@ function bridged_function(b::AbstractBridgeOptimizer, end return func end +# Shortcut to avoid `Variable.throw_if_cannot_unbridge(Variable.bridges(b))` +function bridge_function( + ::AbstractBridgeOptimizer, + value::Union{Number, Enum, AbstractArray{<:Union{Number, Enum}}}) + return value +end + """ unbridged_variable_function(b::AbstractBridgeOptimizer, @@ -975,6 +982,12 @@ function unbridged_function(bridge::AbstractBridgeOptimizer, func::Union{MOI.SingleVariable, MOI.VectorOfVariables}) return func # bridged variables are not allowed in non-bridged constraints end +# Shortcut to avoid `Variable.throw_if_cannot_unbridge(Variable.bridges(b))` +function unbridged_function( + ::AbstractBridgeOptimizer, + value::Union{Number, Enum, AbstractArray{<:Union{Number, Enum}}}) + return value +end """ unbridged_constraint_function( diff --git a/test/Bridges/Bridges.jl b/test/Bridges/Bridges.jl index 040b21f647..d6a5bc3e90 100644 --- a/test/Bridges/Bridges.jl +++ b/test/Bridges/Bridges.jl @@ -6,6 +6,9 @@ end @testset "LazyBridgeOptimizer" begin include("lazy_bridge_optimizer.jl") end +@testset "Variable bridges" begin + include("Variable/Variable.jl") +end @testset "Constraint bridges" begin include("Constraint/Constraint.jl") end diff --git a/test/Bridges/Constraint/slack.jl b/test/Bridges/Constraint/slack.jl index 12e6bfc8eb..2772e33073 100644 --- a/test/Bridges/Constraint/slack.jl +++ b/test/Bridges/Constraint/slack.jl @@ -78,7 +78,7 @@ config = MOIT.TestConfig() for T in [Int, Float64], S in [MOI.GreaterThan{T}, MOI.GreaterThan{T}] for F in [MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}] - @test MOIB.added_constraint_types(MOIB.Constraint.ScalarSlackBridge{T, F, S}) == [(F, MOI.EqualTo{T}), (MOI.SingleVariable, S)] + @test MOIB.added_constraint_types(MOIB.Constraint.ScalarSlackBridge{T, F, S}) == [(F, MOI.EqualTo{T})] end end end @@ -146,7 +146,7 @@ end for T in [Int, Float64], S in [MOI.Nonnegatives, MOI.Nonpositives] for F in [MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}] - @test MOIB.added_constraint_types(MOIB.Constraint.VectorSlackBridge{T, F, S}) == [(F, MOI.Zeros), (MOI.VectorOfVariables, S)] + @test MOIB.added_constraint_types(MOIB.Constraint.VectorSlackBridge{T, F, S}) == [(F, MOI.Zeros)] end end end diff --git a/test/Bridges/Variable/Variable.jl b/test/Bridges/Variable/Variable.jl index f2a99d8443..bc7a5f5a61 100644 --- a/test/Bridges/Variable/Variable.jl +++ b/test/Bridges/Variable/Variable.jl @@ -1,3 +1,18 @@ @testset "Map" begin include("map.jl") end +@testset "Zeros" begin + include("zeros.jl") +end +@testset "Free" begin + include("free.jl") +end +@testset "FlipSign" begin + include("flip_sign.jl") +end +@testset "Vectorize" begin + include("vectorize.jl") +end +@testset "RSOCtoPSD" begin + include("rsoc_to_psd.jl") +end diff --git a/test/Bridges/Variable/flip_sign.jl b/test/Bridges/Variable/flip_sign.jl new file mode 100644 index 0000000000..c0a426b415 --- /dev/null +++ b/test/Bridges/Variable/flip_sign.jl @@ -0,0 +1,60 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("../utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}())) +config = MOIT.TestConfig() + +@testset "NonposToNonneg" begin + bridged_mock = MOIB.Variable.NonposToNonneg{Float64}(mock) + + @testset "lin2v" begin + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [-4, 3, 16, 0], + (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => [[7, 2, -4]]) + MOIT.lin2vtest(bridged_mock, config) + + @test MOI.get(mock, MOI.NumberOfVariables()) == 4 + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 4 + vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + y = vis[4] + @test y.value == -1 + + @test MOI.supports(bridged_mock, MOI.VariablePrimalStart(), MOI.VariableIndex) + MOI.set(bridged_mock, MOI.VariablePrimalStart(), y, 1.0) + x, y_flipped, z, s = MOI.get(mock, MOI.ListOfVariableIndices()) + @test MOI.get(mock, MOI.VariablePrimalStart(), y_flipped) == -1 + end + + @testset "lin4" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, MOI.INFEASIBLE, MOI.INFEASIBLE_POINT, + MOI.INFEASIBILITY_CERTIFICATE) + ) + MOIT.lin4test(bridged_mock, config) + + @test MOI.get(mock, MOI.NumberOfVariables()) == 1 + @test length(MOI.get(mock, MOI.ListOfVariableIndices())) == 1 + @test first(MOI.get(mock, MOI.ListOfVariableIndices())).value ≥ 0 + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 1 + vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test vis == [MOI.VariableIndex(-1)] + test_delete_bridged_variable(bridged_mock, vis[1], MOI.Nonpositives, 1, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + )) + end + + @testset "Delete in vector" begin + MOI.empty!(bridged_mock) + vis, ci = MOI.add_constrained_variables(bridged_mock, MOI.Nonpositives(4)) + test_delete_bridged_variable(bridged_mock, vis[2], MOI.Nonpositives, 4, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + ), used_bridges = 0, used_constraints = 0) + end +end diff --git a/test/Bridges/Variable/free.jl b/test/Bridges/Variable/free.jl new file mode 100644 index 0000000000..ba144cbb63 --- /dev/null +++ b/test/Bridges/Variable/free.jl @@ -0,0 +1,110 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("../utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}())) +config = MOIT.TestConfig() + +bridged_mock = MOIB.Variable.Free{Float64}(mock) + +@testset "solve_multirow_vectoraffine_nonpos" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [0.5, 0.0]) + ), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [0.25, 0.0]) + ) + ) + MOIT.solve_multirow_vectoraffine_nonpos(bridged_mock, config) +end + +@testset "Linear6" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, -100], + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) => [1.0], + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => [-1.0] + )) + MOIT.linear6test(bridged_mock, config) + + loc = MOI.get(bridged_mock, MOI.ListOfConstraints()) + @test length(loc) == 2 + @test !((MOI.VectorOfVariables, MOI.Reals) in loc) + @test (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) in loc + @test (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) in loc + @test MOI.get(mock, MOI.NumberOfVariables()) == 4 + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 2 + vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test vis == MOI.VariableIndex.([-1, -2]) + + cx = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(vis[1].value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cx) == [100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cx) == [0.0] + cy = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(vis[2].value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cy) == [-100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cy) == [0.0] + + test_delete_bridged_variable(bridged_mock, vis[1], MOI.Reals, 2, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + )) + test_delete_bridged_variable(bridged_mock, vis[2], MOI.Reals, 1, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + )) +end + +@testset "Linear7" begin + function set_mock_optimize_linear7Test!(mock) + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, -100], + (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[1.0]], + (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1.0]] + )) + end + set_mock_optimize_linear7Test!(mock) + MOIT.linear7test(bridged_mock, config) + + x, y = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + + cx = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(x.value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cx) == [100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cx) == [0.0] + cy = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(y.value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cy) == [-100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cy) == [0.0] + + @test MOI.supports(bridged_mock, MOI.VariablePrimalStart(), MOI.VariableIndex) + MOI.set(bridged_mock, MOI.VariablePrimalStart(), [x, y], [1.0, -1.0]) + xa, xb, ya, yb = MOI.get(mock, MOI.ListOfVariableIndices()) + @test MOI.get(mock, MOI.VariablePrimalStart(), [xa, xb, ya, yb]) == [1.0, 0.0, 0.0, -1.0] +end + +@testset "Linear11" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 0.0, 1.0, 0.0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0.5, 0.0, 0.5, 0.0])) + MOIT.linear11test(bridged_mock, config) + + vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test vis == MOI.VariableIndex.([-1, -2]) + + test_delete_bridged_variable(bridged_mock, vis[1], MOI.Reals, 2, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + ), used_bridges = 0, used_constraints = 0) + test_delete_bridged_variable(bridged_mock, vis[2], MOI.Reals, 1, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + )) +end diff --git a/test/Bridges/Variable/rsoc_to_psd.jl b/test/Bridges/Variable/rsoc_to_psd.jl new file mode 100644 index 0000000000..a716f3cf50 --- /dev/null +++ b/test/Bridges/Variable/rsoc_to_psd.jl @@ -0,0 +1,43 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("../utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) +config = MOIT.TestConfig() + +bridged_mock = MOIB.Variable.RSOCtoPSD{Float64}(mock) + +@testset "RSOC4" begin + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1.0, 2.0, 1.0, 0.0, 2.0], + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) => [0.25], + (MOI.SingleVariable, MOI.EqualTo{Float64}) => [-0.5], + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => [-1.0], + (MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle) => [[1.0, -0.5, 0.25, -0.5, 0.25, 0.25]]) + mock.eval_variable_constraint_dual = false + MOIT.rotatedsoc4test(bridged_mock, config) + mock.eval_variable_constraint_dual = true + + @testset "Delete" begin + v = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test length(v) == 4 + + message = string("Cannot delete variable as it is constrained with other", + " variables in a `MOI.VectorOfVariables`.") + for i in 1:4 + err = MOI.DeleteNotAllowed(v[i], message) + @test_throws err MOI.delete(bridged_mock, v[i]) + end + + test_delete_bridged_variables(bridged_mock, v, MOI.RotatedSecondOrderCone, 4, ( + (MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle, 0), + (MOI.SingleVariable, MOI.EqualTo{Float64}, 0), + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}, 0), + )) + end +end diff --git a/test/Bridges/Variable/vectorize.jl b/test/Bridges/Variable/vectorize.jl new file mode 100644 index 0000000000..f4be936f14 --- /dev/null +++ b/test/Bridges/Variable/vectorize.jl @@ -0,0 +1,106 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("../utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) +config = MOIT.TestConfig() + +bridged_mock = MOIB.Variable.Vectorize{Float64}(mock) + +@testset "get scalar constraint" begin + x, cx = MOI.add_constrained_variable(bridged_mock, MOI.GreaterThan(1.0)) + fx = MOI.SingleVariable(x) + func = 2.0 * fx + set = MOI.GreaterThan(5.0) + err = MOI.ScalarFunctionConstantNotZero{ + Float64, typeof(func), typeof(set)}(1.0) + @test_throws err MOI.add_constraint(bridged_mock, func + 1.0, set) + + c = MOI.add_constraint(bridged_mock, func, set) + @test MOI.get(bridged_mock, MOI.ConstraintFunction(), c) ≈ func + @test MOI.get(bridged_mock, MOI.ConstraintSet(), c) == set +end + +@testset "exp3 with add_constrained_variable for `y`" begin + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [log(5), 0.0], + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => [0.0], + (MOI.VectorAffineFunction{Float64}, MOI.ExponentialCone) => [[-1.0, log(5)-1, 1/5]]) + + MOI.empty!(bridged_mock) + x = MOI.add_variable(bridged_mock) + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 1 + fx = MOI.SingleVariable(x) + xc = MOI.add_constraint(bridged_mock, 2.0fx, MOI.LessThan(4.0)) + y, yc = MOI.add_constrained_variable(bridged_mock, MOI.LessThan(5.0)) + @test yc.value == y.value == -1 + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 2 + @test length(MOI.get(bridged_mock, MOI.ListOfVariableIndices())) == 2 + @test Set(MOI.get(bridged_mock, MOI.ListOfVariableIndices())) == Set([x, y]) + fy = MOI.SingleVariable(y) + ec = MOI.add_constraint(bridged_mock, + MOIU.operate(vcat, Float64, fx, 1.0, fy), + MOI.ExponentialCone()) + + MOI.optimize!(bridged_mock) + @test MOI.get(bridged_mock, MOI.VariablePrimal(), x) ≈ log(5) + @test MOI.get(bridged_mock, MOI.VariablePrimal(), y) ≈ 5.0 + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), xc) ≈ 2log(5) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), yc) ≈ 5 + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), ec) ≈ [log(5), 1., 5.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), xc) ≈ 0.0 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), yc) ≈ -1/5 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), ec) ≈ [-1., log(5)-1, 1/5] + + err = ErrorException( + "Cannot add two `SingleVariable`-in-`MathOptInterface.LessThan{Float64}`" * + " on the same variable MathOptInterface.VariableIndex(-1)." + ) + @test_throws err MOI.add_constraint(bridged_mock, MOI.SingleVariable(y), MOI.LessThan(4.0)) + + cis = MOI.get(bridged_mock, MOI.ListOfConstraintIndices{ + MOI.VectorAffineFunction{Float64}, MOI.ExponentialCone}()) + @test length(cis) == 1 + + @testset "get `UnknownVariableAttribute``" begin + err = ArgumentError( + "Variable bridge of type `MathOptInterface.Bridges.Variable.VectorizeBridge{Float64,MathOptInterface.Nonpositives}`" * + " does not support accessing the attribute `MathOptInterface.Test.UnknownVariableAttribute()`." + ) + @test_throws err MOI.get(bridged_mock, MOIT.UnknownVariableAttribute(), y) + end + + @testset "set `ConstraintSet`" begin + ci = MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}}(y.value) + attr = MOI.ConstraintSet() + err = MOI.SetAttributeNotAllowed(attr, + "The variable `MathOptInterface.VariableIndex(12345676)` is bridged by the `VectorizeBridge`.") + @test_throws err MOI.set(bridged_mock, attr, ci, MOI.LessThan(4.0)) + end + + @testset "MultirowChange" begin + change = MOI.MultirowChange(y, [(3, 0.0)]) + message = "The change MathOptInterface.MultirowChange{Float64}(MathOptInterface.VariableIndex(-1), Tuple{Int64,Float64}[(3, 0.0)])" * + " contains variables bridged into a function with nonzero constant." + err = MOI.ModifyConstraintNotAllowed(cis[1], change, message) + @test_throws err MOI.modify(bridged_mock, cis[1], change) + end + + @testset "ScalarCoefficientChange" begin + change = MOI.ScalarCoefficientChange(y, 0.0) + attr = MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}() + message = "The change MathOptInterface.ScalarCoefficientChange{Float64}(MathOptInterface.VariableIndex(-1), 0.0)" * + " contains variables bridged into a function with nonzero constant." + err = MOI.ModifyObjectiveNotAllowed(change, message) + @test_throws err MOI.modify(bridged_mock, attr, change) + end + + test_delete_bridged_variable(bridged_mock, y, MOI.LessThan{Float64}, 2, ( + (MOI.VectorOfVariables, MOI.Nonpositives, 0), + )) +end diff --git a/test/Bridges/Variable/zeros.jl b/test/Bridges/Variable/zeros.jl new file mode 100644 index 0000000000..98091fc3a1 --- /dev/null +++ b/test/Bridges/Variable/zeros.jl @@ -0,0 +1,121 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("../utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) +config = MOIT.TestConfig() + +bridged_mock = MOIB.Variable.Zeros{Float64}(mock) + +x, cx = MOI.add_constrained_variable(bridged_mock, MOI.GreaterThan(0.0)) +yz, cyz = MOI.add_constrained_variables(bridged_mock, MOI.Zeros(2)) +y, z = yz +fx = MOI.SingleVariable(x) +fy = MOI.SingleVariable(y) +fz = MOI.SingleVariable(z) +c1, c2 = MOI.add_constraints( + bridged_mock, [1.0fy + 1.0fz, 1.0fx + 1.0fy + 1.0fz], + [MOI.EqualTo(0.0), MOI.GreaterThan(1.0)] +) +#c2 = MOI.add_constraint(bridged_mock, , ) +MOI.set(bridged_mock, MOI.ObjectiveSense(), MOI.MIN_SENSE) +obj = 1.0fx - 1.0fy - 1.0fz +MOI.set(bridged_mock, MOI.ObjectiveFunction{typeof(obj)}(), obj) + +@test MOIB.Variable.unbridged_map(MOIB.bridge(bridged_mock, y), y, MOIB.Variable.IndexInVector(1)) === nothing +@test MOIB.Variable.unbridged_map(MOIB.bridge(bridged_mock, z), z, MOIB.Variable.IndexInVector(2)) === nothing + +err = ErrorException( + "Cannot delete constraint index of bridged constrained variables. Delete" * + " the scalar variable or the vector of variables instead." +) +@test_throws err MOI.delete(bridged_mock, cyz) + +err = ErrorException( + "Cannot add two `VectorOfVariables`-in-`MathOptInterface.Zeros` on the" * + " same first variable MathOptInterface.VariableIndex(-1)." +) +@test_throws err MOI.add_constraint(bridged_mock, MOI.VectorOfVariables(yz), MOI.Zeros(2)) + +err = ErrorException( + "Cannot `VectorOfVariables`-in-`MathOptInterface.Zeros` for" * + " which some variables are bridged but not the first one" * + " `MathOptInterface.VariableIndex(12345679)`." +) +@test_throws err MOI.add_constraint(bridged_mock, MOI.VectorOfVariables([x, y]), MOI.Zeros(2)) + +err = ErrorException( + "Cannot unbridge function because some variables are bridged by" * + " variable bridges that do not support reverse mapping, e.g.," * + " `ZerosBridge`." +) +@test_throws err MOI.get(bridged_mock, MOI.ObjectiveFunction{typeof(obj)}()) +# With `c1`, the function does not contain any variable so it tests that it +# also throws an error even if it never calls `variable_unbridged_function`. +@test_throws err MOI.get(bridged_mock, MOI.ConstraintFunction(), c1) +@test_throws err MOI.get(bridged_mock, MOI.ConstraintFunction(), c2) + +err = ArgumentError( + "Variable bridge of type `MathOptInterface.Bridges.Variable.ZerosBridge{Float64}`" * + " does not support accessing the attribute `MathOptInterface.Test.UnknownVariableAttribute()`." +) +@test_throws err MOI.get(bridged_mock, MOIT.UnknownVariableAttribute(), y) + +@testset "Results" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, [1.0], + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) => 0.0, + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) => 1.0) + ) + MOI.optimize!(bridged_mock) + @test MOI.get(bridged_mock, MOI.VariablePrimal(), x) == 1.0 + @test MOI.get(bridged_mock, MOI.VariablePrimal(), y) == 0.0 + @test MOI.get(bridged_mock, MOI.VariablePrimal(), z) == 0.0 + + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cyz) == zeros(2) + + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cx) == 0.0 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), c1) == 0.0 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), c2) == 1.0 + + err = ArgumentError( + "Bridge of type `MathOptInterface.Bridges.Variable.ZerosBridge{Float64}`" * + " does not support accessing the attribute" * + " `MathOptInterface.ConstraintDual(1)`." + ) + @test_throws err MOI.get(bridged_mock, MOI.ConstraintDual(), cyz) +end + +@testset "Query" begin + @test MOI.get(bridged_mock, MOI.ConstraintFunction(), cyz).variables == yz + @test MOI.get(mock, MOI.NumberOfVariables()) == 1 + @test MOI.get(mock, MOI.ListOfVariableIndices()) == [x] + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 3 + @test MOI.get(bridged_mock, MOI.ListOfVariableIndices()) == [x, y, z] + @test MOI.get(mock, MOI.NumberOfConstraints{MOI.VectorOfVariables, MOI.Zeros}()) == 0 + @test MOI.get(bridged_mock, MOI.NumberOfConstraints{MOI.VectorOfVariables, MOI.Zeros}()) == 1 + @test MOI.get(bridged_mock, MOI.ListOfConstraintIndices{MOI.VectorOfVariables, MOI.Zeros}()) == [cyz] +end + +@testset "SingleVariable objective" begin + err = ErrorException("Using bridged variable in `SingleVariable` function.") + @test_throws err MOI.set(bridged_mock, MOI.ObjectiveFunction{typeof(fy)}(), fy) + MOI.set(bridged_mock, MOI.ObjectiveFunction{typeof(fx)}(), fx) + @test MOI.get(bridged_mock, MOI.ObjectiveFunction{typeof(fx)}()) == fx +end + +@testset "Delete" begin + test_delete_bridged_variables(bridged_mock, yz, MOI.Zeros, 3, ( + (MOI.SingleVariable, MOI.GreaterThan{Float64}, 1), + )) + @test MOI.is_valid(bridged_mock, x) + @test !MOI.is_valid(bridged_mock, y) + @test !MOI.is_valid(bridged_mock, z) +end diff --git a/test/Bridges/lazy_bridge_optimizer.jl b/test/Bridges/lazy_bridge_optimizer.jl index e5b669cd82..3c809127fd 100644 --- a/test/Bridges/lazy_bridge_optimizer.jl +++ b/test/Bridges/lazy_bridge_optimizer.jl @@ -8,6 +8,152 @@ const MOIB = MathOptInterface.Bridges include("utilities.jl") +MOIU.@model( + LPModel, + (), (MOI.EqualTo, MOI.GreaterThan, MOI.LessThan), (), (), + (), (MOI.ScalarAffineFunction,), (), () +) + +@testset "Name test" begin + model = LPModel{Float64}() + bridged = MOIB.full_bridge_optimizer(model, Float64) + MOIT.nametest(bridged) +end + +# Model similar to SDPA format, it gives a good example because it does not +# support a lot hence need a lot of bridges +MOIU.@model(SDPAModel, + (), (MOI.EqualTo,), (MOI.Nonnegatives, MOI.PositiveSemidefiniteConeTriangle), (), + (), (MOI.ScalarAffineFunction,), (MOI.VectorOfVariables,), ()) +MOI.supports_constraint(::SDPAModel{T}, ::Type{MOI.SingleVariable}, ::Type{MOI.GreaterThan{T}}) where {T} = false +MOI.supports_constraint(::SDPAModel{T}, ::Type{MOI.SingleVariable}, ::Type{MOI.LessThan{T}}) where {T} = false +MOI.supports_constraint(::SDPAModel{T}, ::Type{MOI.SingleVariable}, ::Type{MOI.EqualTo{T}}) where {T} = false +MOI.supports_constraint(::SDPAModel, ::Type{MOI.VectorOfVariables}, ::Type{MOI.Reals}) = false +MOI.supports(::SDPAModel{T}, ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}) where {T} = false +MOI.supports(::SDPAModel, ::MOI.ObjectiveFunction{MOI.SingleVariable}) = false + +@testset "Name test" begin + model = SDPAModel{Float64}() + bridged = MOIB.full_bridge_optimizer(model, Float64) + MOIT.nametest(bridged) +end +@testset "SDPA format with $T" for T in [Float64, Int] + model = SDPAModel{T}() + bridged = MOIB.LazyBridgeOptimizer(model) + @testset "Variable" begin + @testset "Nonpositives" begin + @test !MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.Nonpositives) + @test !MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.Nonpositives) + MOIB.add_bridge(bridged, MOIB.Variable.NonposToNonnegBridge{T}) + @test MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.Nonpositives) + @test MOIB.bridge_type(bridged, MOI.Nonpositives) == MOIB.Variable.NonposToNonnegBridge{T} + end + @testset "Zeros" begin + @test !MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.Zeros) + @test !MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.Zeros) + MOIB.add_bridge(bridged, MOIB.Variable.ZerosBridge{T}) + @test MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.Zeros) + @test MOIB.bridge_type(bridged, MOI.Zeros) == MOIB.Variable.ZerosBridge{T} + end + @testset "Free" begin + @test !MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.Reals) + @test !MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.Reals) + MOIB.add_bridge(bridged, MOIB.Variable.FreeBridge{T}) + @test MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.Reals) + @test MOIB.bridge_type(bridged, MOI.Reals) == MOIB.Variable.FreeBridge{T} + end + @testset "Vectorize" begin + @test !MOI.supports_constraint(model, MOI.SingleVariable, MOI.GreaterThan{T}) + @test !MOI.supports_constraint(bridged, MOI.SingleVariable, MOI.GreaterThan{T}) + @test !MOI.supports_constraint(model, MOI.SingleVariable, MOI.LessThan{T}) + @test !MOI.supports_constraint(bridged, MOI.SingleVariable, MOI.LessThan{T}) + @test !MOI.supports_constraint(model, MOI.SingleVariable, MOI.EqualTo{T}) + @test !MOI.supports_constraint(bridged, MOI.SingleVariable, MOI.EqualTo{T}) + MOIB.add_bridge(bridged, MOIB.Variable.VectorizeBridge{T}) + @test MOI.supports_constraint(bridged, MOI.SingleVariable, MOI.GreaterThan{T}) + @test MOIB.bridge_type(bridged, MOI.GreaterThan{T}) == MOIB.Variable.VectorizeBridge{T, MOI.Nonnegatives} + @test MOI.supports_constraint(bridged, MOI.SingleVariable, MOI.LessThan{T}) + @test MOIB.bridge_type(bridged, MOI.LessThan{T}) == MOIB.Variable.VectorizeBridge{T, MOI.Nonpositives} + @test MOI.supports_constraint(bridged, MOI.SingleVariable, MOI.EqualTo{T}) + @test MOIB.bridge_type(bridged, MOI.EqualTo{T}) == MOIB.Variable.VectorizeBridge{T, MOI.Zeros} + end + @testset "RSOCtoPSD" begin + @test !MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.RotatedSecondOrderCone) + @test !MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.RotatedSecondOrderCone) + MOIB.add_bridge(bridged, MOIB.Variable.RSOCtoPSDBridge{T}) + @test !MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.RotatedSecondOrderCone) + MOIB.add_bridge(bridged, MOIB.Constraint.ScalarFunctionizeBridge{T}) + @test MOI.supports_constraint(bridged, MOI.VectorOfVariables, MOI.RotatedSecondOrderCone) + @test MOIB.bridge_type(bridged, MOI.RotatedSecondOrderCone) == MOIB.Variable.RSOCtoPSDBridge{T} + end + @testset "Combining two briges" begin + xy = MOI.add_variables(bridged, 2) + test_delete_bridged_variables(bridged, xy, MOI.Reals, 2, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0)), + used_bridges = 2) + end + end + @testset "Constraint" begin + @testset "Slack" begin + @test !MOI.supports_constraint(bridged, MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone) + MOIB.add_bridge(bridged, MOIB.Constraint.VectorSlackBridge{T}) + @test !MOI.supports_constraint(bridged, MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone) + MOIB.add_bridge(bridged, MOIB.Constraint.ScalarizeBridge{T}) + @test MOI.supports_constraint(bridged, MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone) + @test MOIB.bridge_type(bridged, MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone) == + MOIB.Constraint.VectorSlackBridge{T, MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone} + @test MOI.supports_constraint(bridged, MOI.VectorAffineFunction{T}, MOI.Zeros) + @test MOIB.bridge_type(bridged, MOI.VectorAffineFunction{T}, MOI.Zeros) == + MOIB.Constraint.ScalarizeBridge{T, MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}} + end + @testset "Vectorize" begin + @test !MOI.supports_constraint(bridged, MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}) + MOIB.add_bridge(bridged, MOIB.Constraint.VectorizeBridge{T}) + @test MOI.supports_constraint(bridged, MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}) + @test MOIB.bridge_type(bridged, MOI.ScalarAffineFunction{T}, + MOI.GreaterThan{T}) == + MOIB.Constraint.VectorizeBridge{ + T, MOI.VectorAffineFunction{T}, MOI.Nonnegatives, + MOI.ScalarAffineFunction{T} + } + + end + @testset "Quadratic" begin + @test !MOI.supports_constraint(bridged, MOI.ScalarQuadraticFunction{T}, MOI.GreaterThan{T}) + @test !MOI.supports_constraint(bridged, MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}) + MOIB.add_bridge(bridged, MOIB.Constraint.QuadtoSOCBridge{T}) + @test MOI.supports_constraint(bridged, MOI.ScalarQuadraticFunction{T}, MOI.GreaterThan{T}) + @test MOIB.bridge_type(bridged, MOI.ScalarQuadraticFunction{T}, + MOI.GreaterThan{T}) == MOIB.Constraint.QuadtoSOCBridge{T} + @test MOI.supports_constraint(bridged, MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}) + @test MOIB.bridge_type(bridged, MOI.ScalarQuadraticFunction{T}, + MOI.LessThan{T}) == MOIB.Constraint.QuadtoSOCBridge{T} + end + end +end + +@testset "Continuous Linear" begin + model = SDPAModel{Float64}() + bridged = MOIB.full_bridge_optimizer(model, Float64) + # For `ScalarAffineFunction`-in-`GreaterThan`, + # `Constraint.ScalarSlackBridge` -> `Variable.VectorizeBridge` + # is equivalent to + # `Constraint.VectorizeBridge` -> `Constraint.VectorSlackBridge` + # however, `Variable.VectorizeBridge` do not support modification of the + # set hence it makes some tests of `contlineartest` fail so we disable it. + MOIB.remove_bridge(bridged, MOIB.Constraint.ScalarSlackBridge{Float64}) + exclude = ["partial_start"] # `VariablePrimalStart` not supported. + MOIT.contlineartest(bridged, MOIT.TestConfig(solve=false), exclude) +end + +@testset "Continuous Conic" begin + model = SDPAModel{Float64}() + bridged = MOIB.full_bridge_optimizer(model, Float64) + exclude = ["exp", "pow", "logdet", "rootdets"] + MOIT.contconictest(bridged, MOIT.TestConfig(solve=false), exclude) +end + # Model not supporting RotatedSecondOrderCone MOIU.@model(NoRSOCModel, (), @@ -182,6 +328,14 @@ end @testset "Supports" begin full_bridged_mock = MOIB.full_bridge_optimizer(mock, Float64) + @testset "Mismatch vector/scalar" begin + for S in [MOI.Nonnegatives, MOI.Nonpositives, MOI.Zeros] + @test !MOI.supports_constraint(full_bridged_mock, MOI.SingleVariable, S) + end + for S in [MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, MOI.EqualTo{Float64}] + @test !MOI.supports_constraint(full_bridged_mock, MOI.VectorOfVariables, S) + end + end greater_nonneg_mock = MOIU.MockOptimizer(GreaterNonnegModel{Float64}()) full_bridged_greater_nonneg = MOIB.full_bridge_optimizer( greater_nonneg_mock, Float64) @@ -247,26 +401,3 @@ end end end end - -@testset "Combining two briges" begin - full_bridged_mock = MOIB.full_bridge_optimizer(mock, Float64) - mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1, 1, 0, 1, 1, 0, 1, √2]) - config = MOIT.TestConfig() - MOIT.rootdett1vtest(full_bridged_mock, config) - MOIT.rootdett1ftest(full_bridged_mock, config) - # Dual is not yet implemented for RootDet and GeoMean bridges - ci = first(MOI.get(full_bridged_mock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.RootDetConeTriangle}())) - test_delete_bridge(full_bridged_mock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone, 0), - (MOI.VectorAffineFunction{Float64}, MOI.GeometricMeanCone, 0), - (MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, 0)), - used_bridges = 3) -end - -@testset "Continuous Linear" begin - exclude = ["partial_start"] # VariablePrimalStart not supported. - MOIT.contlineartest(bridged_mock, MOIT.TestConfig(solve=false), exclude) -end - -@testset "Continuous Conic" begin - MOIT.contconictest(MOIB.full_bridge_optimizer(mock, Float64), MOIT.TestConfig(solve=false), ["logdets", "rootdets", "psds"]) -end diff --git a/test/Bridges/utilities.jl b/test/Bridges/utilities.jl index a1ea48ac8a..6a31e9b9fc 100644 --- a/test/Bridges/utilities.jl +++ b/test/Bridges/utilities.jl @@ -13,23 +13,80 @@ function test_delete_bridge( end start_num_bridges = num_bridges() @test MOI.get(m, MOI.NumberOfVariables()) == nvars + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars test_noc(m, F, S, num_bridged) for noc in nocs test_noc(m, noc...) end @test MOI.is_valid(m, ci) MOI.delete(m, ci) - @test_throws MOI.InvalidIndex{typeof(ci)} MOI.delete(m, ci) - try - MOI.delete(m, ci) - catch err - @test err.index == ci - end + @test_throws MOI.InvalidIndex(ci) MOI.delete(m, ci) @test !MOI.is_valid(m, ci) @test num_bridges() == start_num_bridges - used_bridges test_noc(m, F, S, num_bridged - 1) # As the bridge has been removed, if the constraints it has created where not removed, it wouldn't be there to decrease this counter anymore @test MOI.get(m, MOI.NumberOfVariables()) == nvars + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars + for noc in nocs + test_noc(m, noc...) + end +end +function test_delete_bridged_variable( + m::MOIB.AbstractBridgeOptimizer, vi::MOI.VariableIndex, S::Type, + nvars::Int, nocs::Tuple; used_bridges = 1, num_bridged = 1, used_constraints = 1) + function num_bridges() + return count(bridge -> true, values(MOIB.Variable.bridges(m))) + end + start_num_bridges = num_bridges() + @test MOI.get(m, MOI.NumberOfVariables()) == nvars + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars + if S != MOI.Reals + F = S <: MOI.AbstractScalarSet ? MOI.SingleVariable : MOI.VectorOfVariables + test_noc(m, F, S, num_bridged) + end + for noc in nocs + test_noc(m, noc...) + end + @test MOI.is_valid(m, vi) + MOI.delete(m, vi) + @test_throws MOI.InvalidIndex(vi) MOI.delete(m, vi) + @test !MOI.is_valid(m, vi) + @test num_bridges() == start_num_bridges - used_bridges + if S != MOI.Reals + test_noc(m, F, S, num_bridged - used_constraints) + end + @test MOI.get(m, MOI.NumberOfVariables()) == nvars - 1 + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars - 1 + for noc in nocs + test_noc(m, noc...) + end +end +function test_delete_bridged_variables( + m::MOIB.AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}, S::Type, + nvars::Int, nocs::Tuple; used_bridges = 1, num_bridged = 1) + function num_bridges() + return count(bridge -> true, values(MOIB.Variable.bridges(m))) + end + start_num_bridges = num_bridges() + @test MOI.get(m, MOI.NumberOfVariables()) == nvars + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars + if S != MOI.Reals + F = S <: MOI.AbstractScalarSet ? MOI.SingleVariable : MOI.VectorOfVariables + test_noc(m, F, S, num_bridged) + end + for noc in nocs + test_noc(m, noc...) + end + @test all(vi -> MOI.is_valid(m, vi), vis) + MOI.delete(m, vis) + @test_throws MOI.InvalidIndex(vis[1]) MOI.delete(m, vis) + @test all(vi -> !MOI.is_valid(m, vi), vis) + @test num_bridges() == start_num_bridges - used_bridges + if S != MOI.Reals + test_noc(m, F, S, num_bridged - 1) + end + @test MOI.get(m, MOI.NumberOfVariables()) == nvars - length(vis) + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars - length(vis) for noc in nocs test_noc(m, noc...) end