diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 734bbbb07b..1b14865b8f 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -113,6 +113,10 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} bridged_model, ExponentialConeToScalarNonlinearFunctionBridge{T}, ) + MOI.Bridges.add_bridge( + bridged_model, + ComplementsToScalarNonlinearFunctionBridge{T}, + ) return end diff --git a/src/Bridges/Constraint/bridges/ComplementsToScalarNonlinearFunctionBridge.jl b/src/Bridges/Constraint/bridges/ComplementsToScalarNonlinearFunctionBridge.jl new file mode 100644 index 0000000000..b8765177cf --- /dev/null +++ b/src/Bridges/Constraint/bridges/ComplementsToScalarNonlinearFunctionBridge.jl @@ -0,0 +1,300 @@ +# 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. + +""" + ComplementsToScalarNonlinearFunctionBridge{T,F,G} <: + Bridges.Constraint.AbstractBridge + +`ComplementsToScalarNonlinearFunctionBridge` implements the following +reformulation: + + * ``(F, x) \\in \\textsf{Complements}()`` to + ``y - F = 0`` and + ```julia + if isfinite(l) + (x - l) * y <= 0.0 + else + y <= 0 + end + if isfinite(u) + (x - u) * y <= 0.0 + else + y >= 0 + end + ``` + +## Source node + +`ComplementsToScalarNonlinearFunctionBridge` supports: + + * `F` in [`MOI.Complements`](@ref) + +## Target nodes + +`ComplementsToScalarNonlinearFunctionBridge` creates: + + * `G` in [`MOI.EqualTo{T}`](@ref) + * [`MOI.ScalarQuadraticFunction`](@ref) in [`MOI.LessThan{T}`](@ref) + * [`MOI.VariableIndex`](@ref) in [`MOI.Interval{T}`](@ref) +""" +mutable struct ComplementsToScalarNonlinearFunctionBridge{ + T, + F<:Union{ + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, + G, +} <: AbstractBridge + f::F + y::Vector{MOI.VariableIndex} + ci_eq::Vector{MOI.ConstraintIndex{G,MOI.EqualTo{T}}} + ci_lt::Vector{ + MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},MOI.LessThan{T}}, + } + bounds::Vector{NTuple{2,T}} + + function ComplementsToScalarNonlinearFunctionBridge{T,F,G}( + f::F, + ::MOI.Complements, + ) where {T,F,G} + return new{T,F,G}( + f, + MOI.VariableIndex[], + MOI.ConstraintIndex{G,MOI.EqualTo{T}}[], + MOI.ConstraintIndex{ + MOI.ScalarQuadraticFunction{T}, + MOI.LessThan{T}, + }[], + NTuple{2,T}[], + ) + end +end + +const ComplementsToScalarNonlinearFunction{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ComplementsToScalarNonlinearFunctionBridge{T},OT} + +function bridge_constraint( + ::Type{ComplementsToScalarNonlinearFunctionBridge{T,F,G}}, + model::MOI.ModelLike, + f::F, + s::MOI.Complements, +) where { + T, + F<:Union{ + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, + G, +} + # !!! info + # Postpone creation until final_touch. + return ComplementsToScalarNonlinearFunctionBridge{T,F,G}(f, s) +end + +function MOI.supports_constraint( + ::Type{<:ComplementsToScalarNonlinearFunctionBridge{T}}, + ::Type{F}, + ::Type{MOI.Complements}, +) where { + T, + F<:Union{ + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, +} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{ComplementsToScalarNonlinearFunctionBridge{T,F,G}}, +) where {T,F,G} + return Tuple{Type}[(MOI.Interval{T},)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{ComplementsToScalarNonlinearFunctionBridge{T,F,G}}, +) where {T,F,G} + return Tuple{Type,Type}[ + (G, MOI.EqualTo{T}), + (MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}), + ] +end + +function concrete_bridge_type( + ::Type{<:ComplementsToScalarNonlinearFunctionBridge{T}}, + ::Type{F}, + ::Type{MOI.Complements}, +) where { + T, + F<:Union{ + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, +} + G = MOI.Utilities.promote_operation( + -, + T, + MOI.Utilities.scalar_type(F), + MOI.VariableIndex, + ) + return ComplementsToScalarNonlinearFunctionBridge{T,F,G} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::ComplementsToScalarNonlinearFunctionBridge, +) + return copy(bridge.f) +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::ComplementsToScalarNonlinearFunctionBridge, +) + n = MOI.output_dimension(bridge.f) + return MOI.Complements(n) +end + +function MOI.delete( + model::MOI.ModelLike, + bridge::ComplementsToScalarNonlinearFunctionBridge, +) + MOI.delete.(model, bridge.y) + MOI.delete.(model, bridge.ci_eq) + MOI.delete.(model, bridge.ci_lt) + empty!(bridge.bounds) + return +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge, + ::MOI.NumberOfVariables, +)::Int64 + return length(bridge.y) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge, + ::MOI.ListOfVariableIndices, +)::Vector{MOI.VariableIndex} + return copy(bridge.y) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge{T,F,G}, + ::MOI.NumberOfConstraints{G,MOI.EqualTo{T}}, +)::Int64 where {T,F,G} + return length(bridge.ci_eq) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge{T,F,G}, + ::MOI.ListOfConstraintIndices{G,MOI.EqualTo{T}}, +) where {T,F,G} + return copy(bridge.ci_eq) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{T},MOI.LessThan{T}}, +)::Int64 where {T} + return length(bridge.ci_lt) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge{T}, + ::MOI.ListOfConstraintIndices{ + MOI.ScalarQuadraticFunction{T}, + MOI.LessThan{T}, + }, +) where {T} + return copy(bridge.ci_lt) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge{T}, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.Interval{T}}, +)::Int64 where {T} + return length(bridge.y) +end + +function MOI.get( + bridge::ComplementsToScalarNonlinearFunctionBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Interval{T}}, +) where {T} + return map(bridge.y) do y + return MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}(y.value) + end +end + +function MOI.Bridges.needs_final_touch( + ::ComplementsToScalarNonlinearFunctionBridge, +) + return true +end + +function MOI.Bridges.final_touch( + bridge::ComplementsToScalarNonlinearFunctionBridge{T}, + model::MOI.ModelLike, +) where {T} + f = collect(MOI.Utilities.eachscalar(bridge.f)) + N = div(length(f), 2) + final_touch_called_previously = isempty(bridge.bounds) + for i in 1:N + x = convert(MOI.VariableIndex, f[i+N]) + ret = MOI.Utilities.get_bounds(model, T, x) + ret = something(ret, (typemin(T), typemax(T))) + if length(bridge.bounds) < i + # This is the first time calling final_touch + push!(bridge.bounds, ret) + elseif bridge.bounds[i] == ret + # We've called final_touch before, and the bounds match. No need to + # reformulate a second time. + continue + elseif bridge.bounds[i] != ret + # There is a stored bound, and the current bounds do not match. This + # means the model has been modified since the previous call to + # final_touch. We need to delete the bridge and start again. + MOI.delete(model, bridge) + MOI.Bridges.final_touch(bridge, model) + return + end + end + if final_touch_called_previously + return # Nothing to be done + end + for i in 1:N + (l, u), F = bridge.bounds[i], f[i] + y_u = isfinite(l) ? typemax(T) : zero(T) + y_l = isfinite(u) ? typemin(T) : zero(T) + y, _ = MOI.add_constrained_variable(model, MOI.Interval{T}(y_l, y_u)) + push!(bridge.y, y) + # F(x) - y = 0 + g = MOI.Utilities.operate(-, T, F, y) + push!(bridge.ci_eq, MOI.add_constraint(model, g, MOI.EqualTo(zero(T)))) + x = convert(MOI.VariableIndex, f[N+i]) + # (x - b) * y <= 0 + for b in (l, u) + if isfinite(b) + x_less_b = MOI.Utilities.operate(-, T, x, b) + h = MOI.Utilities.operate(*, T, x_less_b, y) + ci = MOI.add_constraint(model, h, MOI.LessThan(zero(T))) + push!(bridge.ci_lt, ci) + end + end + end + return +end diff --git a/src/functions.jl b/src/functions.jl index bc658737c4..d8be6a7143 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -68,6 +68,9 @@ constant(::VariableIndex, ::Type{T}) where {T} = zero(T) Base.copy(x::VariableIndex) = x +Base.isapprox(::Number, ::AbstractScalarFunction; kwargs...) = false +Base.isapprox(::AbstractScalarFunction, ::Number; kwargs...) = false + Base.isapprox(x::VariableIndex, y::VariableIndex; kwargs...) = x == y """ @@ -973,6 +976,13 @@ function Base.convert( return convert(VariableIndex, convert(ScalarAffineFunction{T}, f)) end +function Base.convert(::Type{VariableIndex}, f::ScalarNonlinearFunction) + if f.head != :+ && length(f.args) != 1 + throw(InexactError(:convert, VariableIndex, f)) + end + return convert(VariableIndex, only(f.args)) +end + # ScalarAffineFunction function Base.convert(::Type{ScalarAffineFunction{T}}, α::T) where {T} diff --git a/test/Bridges/Constraint/ComplementsToScalarNonlinearFunctionBridge.jl b/test/Bridges/Constraint/ComplementsToScalarNonlinearFunctionBridge.jl new file mode 100644 index 0000000000..dd7ffbfff5 --- /dev/null +++ b/test/Bridges/Constraint/ComplementsToScalarNonlinearFunctionBridge.jl @@ -0,0 +1,222 @@ +# 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 TestConstraintComplementsToScalarNonlinearFunctionBridge + +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_VectorOfVariables() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [f, x] in Complements(2) + x >= 0.0 + """, + """ + variables: f, x, y + 1.0 * f + -1.0 * y == 0.0 + 1.0 * x * y <= 0.0 + y in Interval(0.0, Inf) + x >= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [f, x] in Complements(2) + x >= 1.0 + """, + """ + variables: f, x, y + 1.0 * f + -1.0 * y == 0.0 + 1.0 * x * y + -1.0 * y <= 0.0 + y in Interval(0.0, Inf) + x >= 1.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [f, x] in Complements(2) + x <= 1.0 + """, + """ + variables: f, x, y + 1.0 * f + -1.0 * y == 0.0 + 1.0 * x * y + -1.0 * y <= 0.0 + y in Interval(-Inf, 0.0) + x <= 1.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [f, x] in Complements(2) + x >= 1.0 + x <= 2.0 + """, + """ + variables: f, x, y + 1.0 * f + -1.0 * y == 0.0 + 1.0 * x * y + -2.0 * y <= 0.0 + 1.0 * x * y + -1.0 * y <= 0.0 + y in Interval(-Inf, Inf) + x >= 1.0 + x <= 2.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [f, x] in Complements(2) + """, + """ + variables: f, x, y + 1.0 * f + -1.0 * y == 0.0 + y in Interval(0.0, 0.0) + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f1, f2, x1, x2 + [f1, f2, x1, x2] in Complements(4) + f1 >= 1.0 + f2 >= 2.0 + x1 >= 3.0 + x2 >= 4.0 + """, + """ + variables: f1, f2, x1, x2, y1, y2 + f1 >= 1.0 + f2 >= 2.0 + x1 >= 3.0 + x2 >= 4.0 + 1.0 * f1 + -1.0 * y1 == 0.0 + 1.0 * f2 + -1.0 * y2 == 0.0 + 1.0 * x1 * y1 + -3.0 * y1 <= 0.0 + y1 in Interval(0.0, Inf) + 1.0 * x2 * y2 + -4.0 * y2 <= 0.0 + y2 in Interval(0.0, Inf) + """, + ) + return +end + +function test_runtests_ScalarAffineFunction() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [2.0 * f + 1.0, x] in Complements(2) + x >= 0.0 + """, + """ + variables: f, x, y + 2.0 * f + 1.0 + -1.0 * y == 0.0 + 1.0 * x * y <= 0.0 + y in Interval(0.0, Inf) + x >= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [2.0 * f + 1.0, x] in Complements(2) + """, + """ + variables: f, x, y + 2.0 * f + 1.0 + -1.0 * y == 0.0 + y in Interval(0.0, 0.0) + """, + ) + return +end + +function test_runtests_ScalarQuadraticFunction() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [2.0 * f * f + 1.0, x] in Complements(2) + x >= 0.0 + """, + """ + variables: f, x, y + 2.0 * f * f + 1.0 + -1.0 * y == 0.0 + 1.0 * x * y <= 0.0 + y in Interval(0.0, Inf) + x >= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + [2.0 * f * f + 1.0, x] in Complements(2) + """, + """ + variables: f, x, y + 2.0 * f * f + 1.0 + -1.0 * y == 0.0 + y in Interval(0.0, 0.0) + """, + ) + return +end + +function test_runtests_ScalarNonlinearFunction() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + VectorNonlinearFunction([sin(f), x]) in Complements(2) + x >= 0.0 + """, + """ + variables: f, x, y + ScalarNonlinearFunction(sin(f) - y) == 0.0 + 1.0 * x * y <= 0.0 + y in Interval(0.0, Inf) + x >= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ComplementsToScalarNonlinearFunctionBridge, + """ + variables: f, x + VectorNonlinearFunction([sin(f), x]) in Complements(2) + """, + """ + variables: f, x, y + ScalarNonlinearFunction(sin(f) - y) == 0.0 + y in Interval(0.0, 0.0) + """, + ) + return +end + +end # module + +TestConstraintComplementsToScalarNonlinearFunctionBridge.runtests()