From 02dc52c05c315cb99c6392387dc0ca05d75d484b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 15 Nov 2024 10:13:00 +1300 Subject: [PATCH] Add Bridges.Constraint.InequalityToComplementsBridge (#2582) --- src/Bridges/Constraint/Constraint.jl | 2 + .../bridges/inequality_to_complements.jl | 151 +++++++++++++++ .../Constraint/inequality_to_complements.jl | 173 ++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 src/Bridges/Constraint/bridges/inequality_to_complements.jl create mode 100644 test/Bridges/Constraint/inequality_to_complements.jl diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 65af0f4d02..d9d3c84074 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -36,6 +36,7 @@ include("bridges/geomean.jl") include("bridges/indicator_activate_on_zero.jl") include("bridges/indicator_flipsign.jl") include("bridges/indicator_sos.jl") +include("bridges/inequality_to_complements.jl") include("bridges/integer_to_zeroone.jl") include("bridges/interval.jl") include("bridges/ltgt_to_interval.jl") @@ -135,6 +136,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} MOI.Bridges.add_bridge(bridged_model, SemiToBinaryBridge{T}) MOI.Bridges.add_bridge(bridged_model, ZeroOneBridge{T}) MOI.Bridges.add_bridge(bridged_model, IntegerToZeroOneBridge{T}) + MOI.Bridges.add_bridge(bridged_model, InequalityToComplementsBridge{T}) # Do not add by default # MOI.Bridges.add_bridge(bridged_model, NumberConversionBridge{T}) # Constraint programming bridges diff --git a/src/Bridges/Constraint/bridges/inequality_to_complements.jl b/src/Bridges/Constraint/bridges/inequality_to_complements.jl new file mode 100644 index 0000000000..c0850f3bcb --- /dev/null +++ b/src/Bridges/Constraint/bridges/inequality_to_complements.jl @@ -0,0 +1,151 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + InequalityToComplementsBridge{T,F,S,G} <: Bridges.Constraint.AbstractBridge + +`InequalityToComplementsBridge` implements the following reformulations: + + * ``f(x) \\ge b`` into ``\\exists y`` such that ``f(x) - b \\perp y \\ge 0`` + * ``f(x) \\le b`` into ``f(x) - b \\perp y \\le 0`` + * ``f(x) = b`` into ``f(x) - b \\perp y`` + +## Source node + +`InequalityToComplementsBridge` supports: + + * `F` in [`MOI.GreaterThan{T}`](@ref) + * `F` in [`MOI.LessThan{T}`](@ref) + * `F` in [`MOI.EqualTo`](@ref) + +## Target nodes + +`InequalityToComplementsBridge` creates: + + * [`MOI.VariableIndex`](@ref) in [`MOI.LessThan{T}`](@ref) + * [`MOI.VariableIndex`](@ref) in [`MOI.GreaterThan{T}`](@ref) + * `G` in [`MOI.Complements`](@ref) +""" +mutable struct InequalityToComplementsBridge{ + T, + F<:MOI.AbstractScalarFunction, + S<:Union{MOI.GreaterThan{T},MOI.LessThan{T},MOI.EqualTo{T}}, + G<:MOI.AbstractVectorFunction, +} <: AbstractBridge + y::MOI.VariableIndex + set::S + ci::MOI.ConstraintIndex{G,MOI.Complements} +end + +const InequalityToComplements{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{InequalityToComplementsBridge{T},OT} + +function _add_y_variable(model, ::MOI.GreaterThan{T}) where {T} + return MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T)))[1] +end + +function _add_y_variable(model, ::MOI.LessThan{T}) where {T} + return MOI.add_constrained_variable(model, MOI.LessThan(zero(T)))[1] +end + +_add_y_variable(model, ::MOI.EqualTo) = MOI.add_variable(model) + +function bridge_constraint( + ::Type{InequalityToComplementsBridge{T,F,S,G}}, + model::MOI.ModelLike, + f::F, + set::S, +) where {T,F,S<:Union{MOI.GreaterThan{T},MOI.LessThan{T},MOI.EqualTo{T}},G} + y = _add_y_variable(model, set) + f_set = MOI.Utilities.operate(-, T, f, MOI.constant(set)) + g = MOI.Utilities.operate(vcat, T, f_set, y) + ci = MOI.add_constraint(model, g, MOI.Complements(2)) + return InequalityToComplementsBridge{T,F,S,G}(y, set, ci) +end + +function MOI.supports_constraint( + ::Type{<:InequalityToComplementsBridge{T}}, + ::Type{F}, + ::Type{<:Union{MOI.GreaterThan{T},MOI.LessThan{T},MOI.EqualTo{T}}}, +) where {T,F<:MOI.AbstractScalarFunction} + return !MOI.Utilities.is_complex(F) +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{InequalityToComplementsBridge{T,F,S,G}}, +) where {T,F,S<:Union{MOI.GreaterThan{T},MOI.LessThan{T}},G} + return Tuple{Type}[(S,)] +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{InequalityToComplementsBridge{T,F,MOI.EqualTo{T},G}}, +) where {T,F,G} + return Tuple{Type}[(MOI.Reals,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{InequalityToComplementsBridge{T,F,S,G}}, +) where {T,F,S,G} + return Tuple{Type,Type}[(G, MOI.Complements)] +end + +function concrete_bridge_type( + ::Type{<:InequalityToComplementsBridge}, + F::Type{<:MOI.AbstractScalarFunction}, + S::Type{<:Union{MOI.GreaterThan{T},MOI.LessThan{T},MOI.EqualTo{T}}}, +) where {T} + G = MOI.Utilities.promote_operation(vcat, T, F, MOI.VariableIndex) + return InequalityToComplementsBridge{T,F,S,G} +end + +function MOI.get(::InequalityToComplementsBridge, ::MOI.NumberOfVariables) + return Int64(1) +end + +function MOI.get( + bridge::InequalityToComplementsBridge, + ::MOI.ListOfVariableIndices, +) + return [bridge.y] +end + +function MOI.get( + ::InequalityToComplementsBridge{T,F,S,G}, + ::MOI.NumberOfConstraints{G,MOI.Complements}, +)::Int64 where {T,F,S,G} + return 1 +end + +function MOI.get( + bridge::InequalityToComplementsBridge{T,F,S,G}, + ::MOI.ListOfConstraintIndices{G,MOI.Complements}, +) where {T,F,S,G} + return [bridge.ci] +end + +function MOI.delete(model::MOI.ModelLike, bridge::InequalityToComplementsBridge) + MOI.delete(model, bridge.y) + MOI.delete(model, bridge.ci) + return +end + +function MOI.get( + model::MOI.ModelLike, + attr::MOI.ConstraintFunction, + bridge::InequalityToComplementsBridge{T}, +) where {T} + g = MOI.get(model, attr, bridge.ci) + f_set = first(MOI.Utilities.scalarize(g)) + return MOI.Utilities.operate(+, T, f_set, MOI.constant(bridge.set)) +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::InequalityToComplementsBridge, +) + return bridge.set +end diff --git a/test/Bridges/Constraint/inequality_to_complements.jl b/test/Bridges/Constraint/inequality_to_complements.jl new file mode 100644 index 0000000000..975816be29 --- /dev/null +++ b/test/Bridges/Constraint/inequality_to_complements.jl @@ -0,0 +1,173 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestConstraintInequalityToComplements + +using Test + +import MathOptInterface as MOI + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_runtests_GreaterThan() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 1.0 * x >= 0.0 + """, + """ + variables: x, y + [1.0 * x, y] in Complements(2) + y >= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 2.0 * x >= 3.0 + """, + """ + variables: x, y + [2.0 * x + -3.0, y] in Complements(2) + y >= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 2.0 * x * x >= 3.0 + """, + """ + variables: x, y + [2.0 * x * x + -3.0, y] in Complements(2) + y >= 0.0 + """, + ) + return +end + +function test_runtests_LessThan() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 1.0 * x <= 0.0 + """, + """ + variables: x, y + [1.0 * x, y] in Complements(2) + y <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 2.0 * x <= 3.0 + """, + """ + variables: x, y + [2.0 * x + -3.0, y] in Complements(2) + y <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 2.0 * x * x <= 3.0 + """, + """ + variables: x, y + [2.0 * x * x + -3.0, y] in Complements(2) + y <= 0.0 + """, + ) + return +end + +function test_runtests_EqualTo() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 1.0 * x == 0.0 + """, + """ + variables: x, y + [1.0 * x, y] in Complements(2) + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 2.0 * x == 3.0 + """, + """ + variables: x, y + [2.0 * x + -3.0, y] in Complements(2) + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.InequalityToComplementsBridge, + """ + variables: x + 2.0 * x * x == 3.0 + """, + """ + variables: x, y + [2.0 * x * x + -3.0, y] in Complements(2) + """, + ) + return +end + +function test_ScalarNonlinearFunction() + # We can't use the standard runtests because ScalarNonlinearFunction does + # not preserve f(x) ≈ (f(x) - g(x)) + g(x) + for set in (MOI.EqualTo(1.0), MOI.LessThan(1.0), MOI.GreaterThan(1.0)) + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.InequalityToComplements{Float64}(inner) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:sin, Any[x]) + c = MOI.add_constraint(model, f, set) + F, S = MOI.VectorNonlinearFunction, MOI.Complements + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + inner_variables = MOI.get(inner, MOI.ListOfVariableIndices()) + @test length(inner_variables) == 2 + u, v = inner_variables + u_sin = MOI.ScalarNonlinearFunction(:sin, Any[u]) + g = MOI.VectorNonlinearFunction([ + MOI.ScalarNonlinearFunction(:-, Any[u_sin, 1.0]), + MOI.ScalarNonlinearFunction(:+, Any[v]), + ]) + @test ≈(MOI.get(inner, MOI.ConstraintFunction(), indices[1]), g) + h = MOI.ScalarNonlinearFunction( + :+, + Any[MOI.ScalarNonlinearFunction(:-, Any[f, 1.0]), 1.0], + ) + @test ≈(MOI.get(model, MOI.ConstraintFunction(), c), h) + end + return +end + +end # module + +TestConstraintInequalityToComplements.runtests()