diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79a4c27..17c550f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,13 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v3 + - name: MOI + shell: julia --project=@. {0} + run: | + using Pkg + Pkg.add([ + PackageSpec(name="MathOptInterface", rev="bl/final_touch"), + ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: diff --git a/src/Bridges/ComplementsVectorizeBridge.jl b/src/Bridges/ComplementsVectorizeBridge.jl index a5a997f..163a70e 100644 --- a/src/Bridges/ComplementsVectorizeBridge.jl +++ b/src/Bridges/ComplementsVectorizeBridge.jl @@ -34,10 +34,14 @@ where `T` is the coefficient type. [`MOI.Zeros`](@ref) depending on the input set type """ -struct ComplementsVectorizeBridge{T,F,S,SV} <: - MOI.Bridges.Constraint.AbstractBridge - constraint::MOI.ConstraintIndex{F,ComplementsWithSetType{SV}} - set_constant::T +mutable struct ComplementsVectorizeBridge{T,F,S,SV} <: + MOI.Bridges.Constraint.AbstractBridge + # The shifted constraint is created in `final_touch` (once the bounds of `x2` + # are set) so it is `nothing` until then. + constraint::Union{Nothing,MOI.ConstraintIndex{F,ComplementsWithSetType{SV}}} + func::MOI.VectorOfVariables + set::ComplementsWithSetType{S} + reformulation::Union{Nothing,AbstractComplementarityRelaxation} end _vector_set_type(::Type{<:MOI.GreaterThan}) = MOI.Nonnegatives @@ -80,13 +84,35 @@ function MOI.Bridges.Constraint.bridge_constraint( set::ComplementsWithSetType{S}, ) where {T,F,S,SV} @assert set.dimension == 2 - x1 = func.variables[1] - x2 = func.variables[2] - c = _set_constant(T, model, set, x2) + # The shift depends on the bounds of `x2`, so it is done in `final_touch`. + return ComplementsVectorizeBridge{T,F,S,SV}(nothing, func, set, nothing) +end + +MOI.Bridges.needs_final_touch(::ComplementsVectorizeBridge) = true + +function MOI.Bridges.final_touch( + bridge::ComplementsVectorizeBridge{T,F,S,SV}, + model::MOI.ModelLike, +) where {T,F,S,SV} + if bridge.constraint !== nothing + return + end + x1 = bridge.func.variables[1] + x2 = bridge.func.variables[2] + c = _set_constant(T, model, bridge.set, x2) # Shift only x2: [x1, x2] → [x1, x2 - c] shifted = MOI.Utilities.operate(vcat, T, one(T) * x1, one(T) * x2 - c) - ci = MOI.add_constraint(model, shifted, ComplementsWithSetType{SV}(2)) - return ComplementsVectorizeBridge{T,F,S,SV}(ci, c) + bridge.constraint = + MOI.add_constraint(model, shifted, ComplementsWithSetType{SV}(2)) + if bridge.reformulation !== nothing + MOI.set( + model, + ComplementarityReformulation(), + bridge.constraint, + bridge.reformulation, + ) + end + return end function MOI.supports_constraint( @@ -122,7 +148,10 @@ function MOI.set( bridge::ComplementsVectorizeBridge, value::AbstractComplementarityRelaxation, ) - MOI.set(model, attr, bridge.constraint, value) + bridge.reformulation = value + if bridge.constraint !== nothing + MOI.set(model, attr, bridge.constraint, value) + end return end @@ -141,39 +170,39 @@ function MOI.Bridges.added_constraint_types( end function MOI.get( - ::ComplementsVectorizeBridge{T,F,S,SV}, + bridge::ComplementsVectorizeBridge{T,F,S,SV}, ::MOI.NumberOfConstraints{F,ComplementsWithSetType{SV}}, )::Int64 where {T,F,S,SV} - return 1 + return bridge.constraint === nothing ? 0 : 1 end function MOI.get( bridge::ComplementsVectorizeBridge{T,F,S,SV}, ::MOI.ListOfConstraintIndices{F,ComplementsWithSetType{SV}}, ) where {T,F,S,SV} - return [bridge.constraint] + CI = MOI.ConstraintIndex{F,ComplementsWithSetType{SV}} + return bridge.constraint === nothing ? CI[] : CI[bridge.constraint] end function MOI.get( - model::MOI.ModelLike, + ::MOI.ModelLike, ::MOI.ConstraintFunction, bridge::ComplementsVectorizeBridge, ) - f = MOI.get(model, MOI.ConstraintFunction(), bridge.constraint) - terms = f.terms - vars = [t.scalar_term.variable for t in terms] - return MOI.VectorOfVariables(vars) + return bridge.func end function MOI.get( ::MOI.ModelLike, ::MOI.ConstraintSet, - ::ComplementsVectorizeBridge{T,F,S}, -) where {T,F,S} - return ComplementsWithSetType{S}(2) + bridge::ComplementsVectorizeBridge, +) + return bridge.set end function MOI.delete(model::MOI.ModelLike, bridge::ComplementsVectorizeBridge) - MOI.delete(model, bridge.constraint) + if bridge.constraint !== nothing + MOI.delete(model, bridge.constraint) + end return end diff --git a/src/Bridges/VerticalBridge.jl b/src/Bridges/VerticalBridge.jl index 173a2d8..ca4f82a 100644 --- a/src/Bridges/VerticalBridge.jl +++ b/src/Bridges/VerticalBridge.jl @@ -31,52 +31,51 @@ to an equality constraint instead. * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref) """ -struct VerticalBridge{T,S<:MOI.AbstractVectorSet} <: - MOI.Bridges.Constraint.AbstractBridge - constraint::MOI.ConstraintIndex{MOI.VectorOfVariables,S} +mutable struct VerticalBridge{T,S<:MOI.AbstractVectorSet} <: + MOI.Bridges.Constraint.AbstractBridge + # The reformulation is done in `final_touch` (once the bounds of the second + # component are set), so `constraint` is `nothing` until then and `func`/`set` + # store the original constraint. + constraint::Union{Nothing,MOI.ConstraintIndex{MOI.VectorOfVariables,S}} equalities::Vector{MOI.ConstraintIndex} slacks::Vector{MOI.VariableIndex} - # Slacks that should be bounded in `final_touch`, together with the - # expression they are equal to, so that their bounds can be computed once the - # bounds of the underlying variables are set. - slacks_to_bound::Vector{Tuple{MOI.VariableIndex,MOI.AbstractScalarFunction}} bounds::Vector{MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}} + func::MOI.AbstractVectorFunction + set::S + reformulation::Union{Nothing,AbstractComplementarityRelaxation} +end + +function VerticalBridge{T,S}( + func::MOI.AbstractVectorFunction, + set::S, +) where {T,S<:MOI.AbstractVectorSet} + return VerticalBridge{T,S}( + nothing, + MOI.ConstraintIndex[], + MOI.VariableIndex[], + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}[], + func, + set, + nothing, + ) end function MOI.Bridges.Constraint.bridge_constraint( ::Type{VerticalBridge{T,MOI.Complements}}, - model::MOI.ModelLike, + ::MOI.ModelLike, func::MOI.AbstractVectorFunction, set::MOI.Complements, ) where {T} - ci, equalities, slacks, slacks_to_bound = - reformulate_to_vertical!(model, T, func, set) - bounds = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}[] - return VerticalBridge{T,MOI.Complements}( - ci, - equalities, - slacks, - slacks_to_bound, - bounds, - ) + return VerticalBridge{T,MOI.Complements}(func, set) end function MOI.Bridges.Constraint.bridge_constraint( ::Type{VerticalBridge{T,ComplementsWithSetType{S}}}, - model::MOI.ModelLike, + ::MOI.ModelLike, func::MOI.AbstractVectorFunction, set::ComplementsWithSetType{S}, ) where {T,S} - ci, equalities, slacks, slacks_to_bound = - reformulate_to_vertical!(model, T, func, set) - bounds = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}[] - return VerticalBridge{T,ComplementsWithSetType{S}}( - ci, - equalities, - slacks, - slacks_to_bound, - bounds, - ) + return VerticalBridge{T,ComplementsWithSetType{S}}(func, set) end function MOI.supports_constraint( @@ -136,17 +135,30 @@ function MOI.get(bridge::VerticalBridge, ::MOI.ListOfVariableIndices) end function MOI.get( - ::VerticalBridge{T,S}, + bridge::VerticalBridge{T,S}, ::MOI.NumberOfConstraints{MOI.VectorOfVariables,S}, )::Int64 where {T,S} - return 1 + return bridge.constraint === nothing ? 0 : 1 end function MOI.get( bridge::VerticalBridge{T,S}, ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}, ) where {T,S} - return [bridge.constraint] + CI = MOI.ConstraintIndex{MOI.VectorOfVariables,S} + return bridge.constraint === nothing ? CI[] : CI[bridge.constraint] +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::VerticalBridge, +) + return bridge.func +end + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, bridge::VerticalBridge) + return bridge.set end function MOI.get( @@ -186,7 +198,9 @@ function MOI.get( end function MOI.delete(model::MOI.ModelLike, bridge::VerticalBridge) - MOI.delete(model, bridge.constraint) + if bridge.constraint !== nothing + MOI.delete(model, bridge.constraint) + end for ci in bridge.equalities MOI.delete(model, ci) end @@ -203,13 +217,21 @@ function MOI.Bridges.final_touch( bridge::VerticalBridge{T}, model::MOI.ModelLike, ) where {T} - # Propagate the bounds of each slack's expression to the slack variable so - # that bridges requiring a finite domain (e.g. `SOS1ToMILPBridge`) can be - # applied. - for (s, expr) in bridge.slacks_to_bound - if any(ci -> ci.value == s.value, bridge.bounds) - continue # The slack is already bounded. - end + if bridge.constraint !== nothing + return + end + # Reformulate in `final_touch` so that the bounds needed to decide whether an + # unbounded second component turns the constraint into an equality, and to + # bound the slacks, are available (`SpecifySetTypeBridge` and + # `SplitIntervalBridge`, which set those bounds, run first). + ci, equalities, slacks, slacks_to_bound = + reformulate_to_vertical!(model, T, bridge.func, bridge.set) + bridge.constraint = ci + append!(bridge.equalities, equalities) + append!(bridge.slacks, slacks) + # Bound each slack from the bounds of the expression it is equal to, so that + # bridges requiring a finite domain (e.g. `SOS1ToMILPBridge`) can be applied. + for (s, expr) in slacks_to_bound b = _expr_bounds(model, T, expr) if b !== nothing push!( @@ -218,6 +240,14 @@ function MOI.Bridges.final_touch( ) end end + if bridge.reformulation !== nothing + MOI.set( + model, + ComplementarityReformulation(), + bridge.constraint, + bridge.reformulation, + ) + end return end @@ -235,7 +265,10 @@ function MOI.set( bridge::VerticalBridge, value::AbstractComplementarityRelaxation, ) - MOI.set(model, attr, bridge.constraint, value) + bridge.reformulation = value + if bridge.constraint !== nothing + MOI.set(model, attr, bridge.constraint, value) + end return end diff --git a/test/test_optimizer.jl b/test/test_optimizer.jl index c62badb..d5dfe95 100644 --- a/test/test_optimizer.jl +++ b/test/test_optimizer.jl @@ -376,6 +376,9 @@ function test_ComplementarityReformulation_through_ComplementsVectorize() attr = MathOptComplements.ComplementarityReformulation() relax = MathOptComplements.FischerBurmeisterRelaxation(1e-8) MOI.set(model, attr, ci, relax) + # `ComplementsVectorizeBridge` creates its (shifted) constraint in + # `final_touch`, applying the reformulation set above. + MOI.Bridges.final_touch(model) # ComplementsVectorize shifts GreaterThan → Nonnegatives, check child exists S = MathOptComplements.ComplementsWithSetType{MOI.Nonnegatives} @test MOI.get(