Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
73 changes: 51 additions & 22 deletions src/Bridges/ComplementsVectorizeBridge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand All @@ -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
115 changes: 74 additions & 41 deletions src/Bridges/VerticalBridge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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!(
Expand All @@ -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

Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions test/test_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading