diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 47d23ab..5d1d9c7 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -19,6 +19,7 @@ include("SplitIntervalBridge.jl") include("FlipSignBridge.jl") include("NonlinearBridge.jl") include("ToSOS1Bridge.jl") +include("ToZerosBridge.jl") """ add_all_bridges(model::MOI.ModelLike, ::Type{T} = Float64) @@ -40,6 +41,7 @@ function add_all_bridges(model::MOI.ModelLike, ::Type{T} = Float64) where {T} MOI.Bridges.add_bridge(model, SplitIntervalBridge{T}) MOI.Bridges.add_bridge(model, FlipSignBridge{T}) MOI.Bridges.add_bridge(model, ToSOS1Bridge{T}) + MOI.Bridges.add_bridge(model, ToZerosBridge{T}) MOI.Bridges.add_bridge(model, VerticalBridge{T}) MOI.Bridges.add_bridge(model, NonlinearBridge{T}) return diff --git a/src/Bridges/ToZerosBridge.jl b/src/Bridges/ToZerosBridge.jl new file mode 100644 index 0000000..97edd6c --- /dev/null +++ b/src/Bridges/ToZerosBridge.jl @@ -0,0 +1,105 @@ +# Copyright (c) 2025 François Pacaud, Benoît Legat, and contributors +# +# 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. + +""" + ToZerosBridge{T} <: MOI.Bridges.Constraint.AbstractBridge + +`ToZerosBridge` implements the following reformulation: + + * `[x₁, x₂]` in `ComplementsWithSetType{Zeros}` into `[x₁]` in + [`MOI.Zeros`](@ref) + +`ComplementsWithSetType{Zeros}` means that the activity `x₁` is complementary +to a variable `x₂` whose reference set is a single point (it is either free or +fixed). In both cases the complementarity condition forces the activity to be +zero, so the constraint reduces to `x₁ = 0`. + +## Source node + +`ToZerosBridge` supports: + + * [`MOI.VectorOfVariables`](@ref) in + [`ComplementsWithSetType{MOI.Zeros}`](@ref) + +## Target nodes + +`ToZerosBridge` creates: + + * [`MOI.VectorOfVariables`](@ref) in [`MOI.Zeros`](@ref) + +""" +struct ToZerosBridge{T} <: MOI.Bridges.Constraint.AbstractBridge + zeros::MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Zeros} + func::MOI.VectorOfVariables +end + +function MOI.Bridges.Constraint.bridge_constraint( + ::Type{ToZerosBridge{T}}, + model::MOI.ModelLike, + func::MOI.VectorOfVariables, + set::ComplementsWithSetType{MOI.Zeros}, +) where {T} + @assert set.dimension == 2 + x1 = func.variables[1] + ci = MOI.add_constraint(model, MOI.VectorOfVariables([x1]), MOI.Zeros(1)) + return ToZerosBridge{T}(ci, func) +end + +function MOI.supports_constraint( + ::Type{<:ToZerosBridge}, + ::Type{MOI.VectorOfVariables}, + ::Type{ComplementsWithSetType{MOI.Zeros}}, +) + return true +end + +function MOI.Bridges.Constraint.concrete_bridge_type( + ::Type{<:ToZerosBridge{T}}, + ::Type{MOI.VectorOfVariables}, + ::Type{ComplementsWithSetType{MOI.Zeros}}, +) where {T} + return ToZerosBridge{T} +end + +# Bridge metadata + +function MOI.Bridges.added_constrained_variable_types(::Type{<:ToZerosBridge}) + return Tuple{Type}[] +end + +function MOI.Bridges.added_constraint_types(::Type{ToZerosBridge{T}}) where {T} + return Tuple{Type,Type}[(MOI.VectorOfVariables, MOI.Zeros)] +end + +function MOI.get( + ::ToZerosBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.Zeros}, +)::Int64 + return 1 +end + +function MOI.get( + bridge::ToZerosBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.Zeros}, +) + return [bridge.zeros] +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::ToZerosBridge, +) + return bridge.func +end + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, ::ToZerosBridge) + return ComplementsWithSetType{MOI.Zeros}(2) +end + +function MOI.delete(model::MOI.ModelLike, bridge::ToZerosBridge) + MOI.delete(model, bridge.zeros) + return +end diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 473c752..db5816c 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -205,13 +205,13 @@ function _sos1_bridge_type( return Bridges.ToSOS1Bridge{T} end -# VOV-in-Zeros → ToSOS1Bridge (trivial complementarity) +# VOV-in-Zeros → ToZerosBridge (trivial complementarity: activity is zero) function _sos1_bridge_type( ::Type{T}, ::Type{MOI.VectorOfVariables}, ::Type{MOI.Zeros}, ) where {T} - return Bridges.ToSOS1Bridge{T} + return Bridges.ToZerosBridge{T} end # Any non-VOV function → VerticalBridge (create slacks, then re-enter as VOV) diff --git a/test/Bridges/test_ToZerosBridge.jl b/test/Bridges/test_ToZerosBridge.jl new file mode 100644 index 0000000..6105272 --- /dev/null +++ b/test/Bridges/test_ToZerosBridge.jl @@ -0,0 +1,41 @@ +# Copyright (c) 2025 François Pacaud, Benoît Legat, and contributors +# +# 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 TestToZerosBridge + +using Test + +import MathOptComplements +import MathOptInterface as MOI + +const M = "TestToZerosBridge.MathOptComplements" + +function runtests() + is_test(name) = startswith("$name", "test_") + @testset "$name" for name in filter(is_test, names(@__MODULE__; all = true)) + getfield(@__MODULE__, name)() + end + return +end + +function test_ToZerosBridge() + MOI.Bridges.runtests( + MathOptComplements.Bridges.ToZerosBridge, + """ + variables: x, y + [x, y] in $M.ComplementsWithSetType{MOI.Zeros}(2) + """, + """ + variables: x, y + [x] in Zeros(1) + """; + cannot_unbridge = true, + ) + return +end + +end # TestToZerosBridge + +TestToZerosBridge.runtests() diff --git a/test/test_optimizer.jl b/test/test_optimizer.jl index 02e8352..6c61380 100644 --- a/test/test_optimizer.jl +++ b/test/test_optimizer.jl @@ -197,13 +197,15 @@ function test_Optimizer_bridge_dispatch() MOI.VectorAffineFunction{Float64}, S, ) == MathOptComplements.Bridges.NonlinearBridge{Float64,MOI.Nonnegatives} - # SOS1 path: VectorOfVariables in ComplementsWithSetType{Zeros} → ToSOS1Bridge + # SOS1 path: VectorOfVariables in ComplementsWithSetType{Zeros} → + # ToZerosBridge (the activity is complementary to a fixed/free variable, so + # it is forced to zero). opt_sos1 = MathOptComplements.Optimizer( MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64), ) S_zeros = MathOptComplements.ComplementsWithSetType{MOI.Zeros} @test MOI.Bridges.bridge_type(opt_sos1, MOI.VectorOfVariables, S_zeros) == - MathOptComplements.Bridges.ToSOS1Bridge{Float64} + MathOptComplements.Bridges.ToZerosBridge{Float64} return end