From 6f167300c4ddb5cdcb040bc4c231112e16a5ce9a Mon Sep 17 00:00:00 2001 From: Shadi Akiki Date: Fri, 27 Nov 2020 14:45:04 +0200 Subject: [PATCH] Add bridge that converts upper/lower constraints to interval constraints. Closes #1193 (#1195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Benoît Legat --- .gitignore | 1 + docs/src/apireference.md | 2 + src/Bridges/Constraint/Constraint.jl | 5 + src/Bridges/Constraint/ltgt_to_interval.jl | 72 +++++++++++ src/Bridges/Constraint/set_map.jl | 3 + test/Bridges/Constraint/ltgt_to_interval.jl | 130 ++++++++++++++++++++ test/Bridges/lazy_bridge_optimizer.jl | 9 +- 7 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/Bridges/Constraint/ltgt_to_interval.jl create mode 100644 test/Bridges/Constraint/ltgt_to_interval.jl diff --git a/.gitignore b/.gitignore index 58d74eb7ad..fe01be8354 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ docs/build/ docs/site/ test/Benchmarks/*.json Manifest.toml +*.swp diff --git a/docs/src/apireference.md b/docs/src/apireference.md index aa870d334c..68b3ace757 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -799,6 +799,8 @@ Bridges.Constraint.AbstractBridge Below is the list of constraint bridges implemented in this package. ```@docs +Bridges.Constraint.GreaterToIntervalBridge +Bridges.Constraint.LessToIntervalBridge Bridges.Constraint.GreaterToLessBridge Bridges.Constraint.LessToGreaterBridge Bridges.Constraint.NonnegToNonposBridge diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 934f50f701..54be396bed 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -81,6 +81,11 @@ Add all bridges defined in the `Bridges.Constraint` submodule to `bridged_model`. The coefficient type used is `T`. """ function add_all_bridges(bridged_model, ::Type{T}) where {T} + if T <: AbstractFloat + MOIB.add_bridge(bridged_model, GreaterToIntervalBridge{T}) + MOIB.add_bridge(bridged_model, LessToIntervalBridge{T}) + end + MOIB.add_bridge(bridged_model, GreaterToLessBridge{T}) MOIB.add_bridge(bridged_model, LessToGreaterBridge{T}) MOIB.add_bridge(bridged_model, NonnegToNonposBridge{T}) diff --git a/src/Bridges/Constraint/ltgt_to_interval.jl b/src/Bridges/Constraint/ltgt_to_interval.jl new file mode 100644 index 0000000000..c413f43f72 --- /dev/null +++ b/src/Bridges/Constraint/ltgt_to_interval.jl @@ -0,0 +1,72 @@ +# The code here is mostly copied from the flip_sign.jl code for FlipSignBridge and GreaterToLessBridge + +""" + AbstractToIntervalBridge{T, S1, F} + +Bridge a `F`-in-`Interval` constraint into an `F`-in-`Interval{T}` constraint where we have either: +* `S1 = MOI.GreaterThan{T}` +* `S1 = MOI.LessThan{T}` + +The `F`-in-`Interval{T}` constraint is stored in the `constraint` +field by convention. +It is required that T be a AbstractFloat type because otherwise +typemin and typemax would either be not implemented (e.g. BigInt) +or would not give infinite value (e.g. Int). +""" +abstract type AbstractToIntervalBridge{ + T<:AbstractFloat, S1<:MOI.AbstractScalarSet, + F<:MOI.AbstractScalarFunction} <: SetMapBridge{T, MOI.Interval{T}, S1, F, F} end + +# The function map is the identity. It is also an involution, symmetric, and a symmetric involution. +map_function(::Type{<:AbstractToIntervalBridge{T}}, func) where {T} = func +inverse_map_function(::Type{<:AbstractToIntervalBridge}, func) = func +adjoint_map_function(::Type{<:AbstractToIntervalBridge}, func) = func +inverse_adjoint_map_function(::Type{<:AbstractToIntervalBridge}, func) = func + +# FIXME are these modify functions necessary? +function MOI.modify(model::MOI.ModelLike, bridge::AbstractToIntervalBridge, + change::MOI.ScalarCoefficientChange) + MOI.modify(model, bridge.constraint, change) +end +function MOI.modify(model::MOI.ModelLike, bridge::AbstractToIntervalBridge, + change::MOI.MultirowChange{T}) where T + MOI.modify(model, bridge.constraint, change) +end + +""" + GreaterToIntervalBridge{T, F<:MOI.AbstractScalarFunction} <: + AbstractToIntervalBridge{T, MOI.GreaterThan{T}, F} + +Transforms a `F`-in-`GreaterThan{T}` constraint into an `F`-in-`Interval{T}` +constraint. +""" +struct GreaterToIntervalBridge{T, F<:MOI.AbstractScalarFunction} <: + AbstractToIntervalBridge{T, MOI.GreaterThan{T}, F} + constraint::CI{F, MOI.Interval{T}} +end +map_set(::Type{<:GreaterToIntervalBridge}, set::MOI.GreaterThan) = MOI.Interval(set.lower, typemax(set.lower)) +inverse_map_set(::Type{<:GreaterToIntervalBridge}, set::MOI.Interval) = MOI.GreaterThan(set.lower) +function concrete_bridge_type(::Type{<:GreaterToIntervalBridge{T}}, + F::Type{<:MOI.AbstractScalarFunction}, + ::Type{MOI.GreaterThan{T}}) where T + return GreaterToIntervalBridge{T, F} +end + +""" + LessToIntervalBridge{T, F<:MOI.AbstractScalarFunction} <: + AbstractToIntervalBridge{T, MOI.LessThan{T}, F} + +Transforms a `F`-in-`LessThan{T}` constraint into an `F`-in-`Interval{T}` +constraint. +""" +struct LessToIntervalBridge{T, F<:MOI.AbstractScalarFunction} <: + AbstractToIntervalBridge{T, MOI.LessThan{T}, F} + constraint::CI{F, MOI.Interval{T}} +end +map_set(::Type{<:LessToIntervalBridge}, set::MOI.LessThan) = MOI.Interval(typemin(set.upper), set.upper) +inverse_map_set(::Type{<:LessToIntervalBridge}, set::MOI.Interval) = MOI.LessThan(set.upper) +function concrete_bridge_type(::Type{<:LessToIntervalBridge{T}}, + F::Type{<:MOI.AbstractScalarFunction}, + ::Type{MOI.LessThan{T}}) where T + return LessToIntervalBridge{T, F} +end diff --git a/src/Bridges/Constraint/set_map.jl b/src/Bridges/Constraint/set_map.jl index 9edd796c23..b404379d02 100644 --- a/src/Bridges/Constraint/set_map.jl +++ b/src/Bridges/Constraint/set_map.jl @@ -97,3 +97,6 @@ const NonposToNonneg{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{NonposToNonne include("rsoc.jl") const RSOC{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCBridge{T}, OT} const SOCR{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SOCRBridge{T}, OT} +include("ltgt_to_interval.jl") +const GreaterToInterval{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{GreaterToIntervalBridge{T}, OT} +const LessToInterval{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{LessToIntervalBridge{T}, OT} diff --git a/test/Bridges/Constraint/ltgt_to_interval.jl b/test/Bridges/Constraint/ltgt_to_interval.jl new file mode 100644 index 0000000000..ea7f0cd8d3 --- /dev/null +++ b/test/Bridges/Constraint/ltgt_to_interval.jl @@ -0,0 +1,130 @@ +# These tests are mostly copies of the flip_sign.jl tests for GreaterToLess + +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 "GreaterToInterval" begin + bridged_mock = MOIB.Constraint.GreaterToInterval{Float64}(mock) + + MOIT.basic_constraint_tests( + bridged_mock, config, + include = [(F, S) + for F in [MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64}] + for S in [MOI.GreaterThan{Float64}]]) + + 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, -100.0])) + MOIT.linear6test(bridged_mock, config) + + ci = first(MOI.get(bridged_mock, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}())) + + @testset "$attr" for attr in [MOI.ConstraintPrimalStart(), MOI.ConstraintDualStart()] + @test MOI.supports(bridged_mock, attr, typeof(ci)) + MOI.set(bridged_mock, attr, ci, 2.0) + @test MOI.get(bridged_mock, attr, ci) ≈ 2.0 + end + + test_delete_bridge(bridged_mock, ci, 2, + ((MOI.ScalarAffineFunction{Float64}, + MOI.Interval{Float64}, 0), + )) +end + + +@testset "LessToInterval" begin + bridged_mock = MOIB.Constraint.LessToInterval{Float64}(mock) + + MOIT.basic_constraint_tests( + bridged_mock, config, + include = [(F, S) + for F in [MOI.SingleVariable, MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64}] + for S in [MOI.LessThan{Float64}]]) + + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.0]), + MOI.FEASIBLE_POINT, + (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) => [-1.0] + ), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [2.0]), + MOI.FEASIBLE_POINT, + (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) => [-1.0] + ) + ) + MOIT.solve_set_scalaraffine_lessthan(bridged_mock, config) + + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.0]), + MOI.FEASIBLE_POINT, + (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) => [-1.0] + ), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [0.5]), + MOI.FEASIBLE_POINT, + (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) => [-0.5] + ) + ) + MOIT.solve_coef_scalaraffine_lessthan(bridged_mock, config) + + ci = first(MOI.get(bridged_mock, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}}())) + + @testset "$attr" for attr in [MOI.ConstraintPrimalStart(), MOI.ConstraintDualStart()] + @test MOI.supports(bridged_mock, attr, typeof(ci)) + MOI.set(bridged_mock, attr, ci, 2.0) + @test MOI.get(bridged_mock, attr, ci) ≈ 2.0 + end + + test_delete_bridge(bridged_mock, ci, 1, + ((MOI.ScalarAffineFunction{Float64}, + MOI.Interval{Float64}, 0),)) +end + + +# Define a dummy optimizer that only supports intervals +# and use it in the below unmocked test +mutable struct Optimizer <: MOI.AbstractOptimizer + function Optimizer() + return new() + end +end + +MOI.get(model::Optimizer, ::MOI.SolverName) = "OnlyIntervalOptimizer" + +MOI.supports_constraint(::Optimizer, ::Type{MOI.ScalarAffineFunction{Float64}}, ::Type{MOI.Interval{Float64}}) = true + +@testset "GreaterOrLessToInterval_unmocked" begin + # model supports Interval but not LessThan or GreaterThan + model = Optimizer() + @test MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) + @test !MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) + @test !MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) + + # bridged model supports all + bridged = MOIB.Constraint.GreaterToInterval{Float64}(MOIB.Constraint.LessToInterval{Float64}(model)) + @test MOI.supports_constraint(bridged, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) + @test MOI.supports_constraint(bridged, MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) + @test MOI.supports_constraint(bridged, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) + + # bridged model with Bridges.full_bridge_optimizer + bridged2 = MOIB.full_bridge_optimizer(model, Float64) + @test MOI.supports_constraint(bridged2, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) + @test MOI.supports_constraint(bridged2, MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) + @test MOI.supports_constraint(bridged2, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) + +end diff --git a/test/Bridges/lazy_bridge_optimizer.jl b/test/Bridges/lazy_bridge_optimizer.jl index 9532b591d7..6ea48ec426 100644 --- a/test/Bridges/lazy_bridge_optimizer.jl +++ b/test/Bridges/lazy_bridge_optimizer.jl @@ -768,11 +768,12 @@ end @test !MOIB.is_variable_bridged(bridged, cy) @test MOIB.bridge(bridged, cy) isa MOIB.Constraint.RSOCBridge{T} @test sprint(MOIB.print_graph, bridged) == MOI.Utilities.replace_acronym(""" -Bridge graph with 4 variable nodes, 9 constraint nodes and 0 objective nodes. +Bridge graph with 5 variable nodes, 11 constraint nodes and 0 objective nodes. [1] constrained variables in `MOI.RotatedSecondOrderCone` are supported (distance 2) by adding free variables and then constrain them, see (3). [2] constrained variables in `MOI.PositiveSemidefiniteConeTriangle` are not supported [3] constrained variables in `MOI.SecondOrderCone` are supported (distance 2) by adding free variables and then constrain them, see (1). [4] constrained variables in `MOI.Nonnegatives` are supported (distance 2) by adding free variables and then constrain them, see (6). + [5] constrained variables in `MOI.Interval{$T}` are supported (distance 3) by adding free variables and then constrain them, see (8). (1) `MOI.VectorOfVariables`-in-`MOI.SecondOrderCone` constraints are bridged (distance 1) by $(MOIB.Constraint.VectorFunctionizeBridge{T,MOI.SecondOrderCone}). (2) `MOI.VectorAffineFunction{$T}`-in-`MOI.RotatedSecondOrderCone` constraints are bridged (distance 1) by $(MOIB.Constraint.RSOCBridge{T,MOI.VectorAffineFunction{T},MOI.VectorAffineFunction{T}}). (3) `MOI.VectorOfVariables`-in-`MOI.RotatedSecondOrderCone` constraints are bridged (distance 1) by $(MOIB.Constraint.RSOCBridge{T,MOI.VectorAffineFunction{T},MOI.VectorOfVariables}). @@ -780,8 +781,10 @@ Bridge graph with 4 variable nodes, 9 constraint nodes and 0 objective nodes. (5) `MOI.VectorOfVariables`-in-`MOI.PositiveSemidefiniteConeTriangle` constraints are not supported (6) `MOI.VectorOfVariables`-in-`MOI.Nonnegatives` constraints are bridged (distance 1) by $(MOIB.Constraint.NonnegToNonposBridge{T,MOI.VectorAffineFunction{T},MOI.VectorOfVariables}). (7) `MOI.SingleVariable`-in-`MOI.GreaterThan{$T}` constraints are bridged (distance 1) by $(MOIB.Constraint.GreaterToLessBridge{T,MOI.ScalarAffineFunction{T},MOI.SingleVariable}). - (8) `MOI.SingleVariable`-in-`MOI.EqualTo{$T}` constraints are bridged (distance 1) by $(MOIB.Constraint.VectorizeBridge{T,MOI.VectorAffineFunction{T},MOI.Zeros,MOI.SingleVariable}). - (9) `MOI.SingleVariable`-in-`MOI.LessThan{$T}` constraints are bridged (distance 1) by $(MOIB.Constraint.LessToGreaterBridge{T,MOI.ScalarAffineFunction{T},MOI.SingleVariable}). + (8) `MOI.SingleVariable`-in-`MOI.Interval{$T}` constraints are bridged (distance 2) by $(MOIB.Constraint.ScalarFunctionizeBridge{T,MOI.Interval{T}}). + (9) `MOI.ScalarAffineFunction{$T}`-in-`MOI.Interval{$T}` constraints are bridged (distance 1) by $(MOIB.Constraint.SplitIntervalBridge{T,MOI.ScalarAffineFunction{T},MOI.Interval{T},MOI.GreaterThan{T},MOI.LessThan{T}}). + (10) `MOI.SingleVariable`-in-`MOI.LessThan{$T}` constraints are bridged (distance 1) by $(MOIB.Constraint.LessToGreaterBridge{T,MOI.ScalarAffineFunction{T},MOI.SingleVariable}). + (11) `MOI.SingleVariable`-in-`MOI.EqualTo{$T}` constraints are bridged (distance 1) by $(MOIB.Constraint.VectorizeBridge{T,MOI.VectorAffineFunction{T},MOI.Zeros,MOI.SingleVariable}). """) end