diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 5193581..47d23ab 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -10,8 +10,7 @@ import MathOptInterface as MOI using ..MathOptComplements: ComplementsWithSetType, AbstractComplementarityRelaxation, - ComplementarityReformulation, - _remove_bounds! + ComplementarityReformulation include("VerticalBridge.jl") include("SpecifySetTypeBridge.jl") diff --git a/src/Bridges/ComplementsVectorizeBridge.jl b/src/Bridges/ComplementsVectorizeBridge.jl index 6a243e1..a5a997f 100644 --- a/src/Bridges/ComplementsVectorizeBridge.jl +++ b/src/Bridges/ComplementsVectorizeBridge.jl @@ -40,15 +40,11 @@ struct ComplementsVectorizeBridge{T,F,S,SV} <: set_constant::T end -function _vector_set_type(::Type{<:MOI.GreaterThan}) - return MOI.Nonnegatives -end -function _vector_set_type(::Type{<:MOI.LessThan}) - return MOI.Nonpositives -end -function _vector_set_type(::Type{<:MOI.EqualTo}) - return MOI.Zeros -end +_vector_set_type(::Type{<:MOI.GreaterThan}) = MOI.Nonnegatives + +_vector_set_type(::Type{<:MOI.LessThan}) = MOI.Nonpositives + +_vector_set_type(::Type{<:MOI.EqualTo}) = MOI.Zeros function _set_constant( ::Type{T}, @@ -58,6 +54,7 @@ function _set_constant( ) where {T} return MOI.Utilities.get_bounds(model, T, x2)[1] end + function _set_constant( ::Type{T}, model, @@ -66,6 +63,7 @@ function _set_constant( ) where {T} return MOI.Utilities.get_bounds(model, T, x2)[2] end + function _set_constant( ::Type{T}, model, @@ -94,12 +92,8 @@ end function MOI.supports_constraint( ::Type{<:ComplementsVectorizeBridge}, ::Type{MOI.VectorOfVariables}, - ::Type{ - <:ComplementsWithSetType{ - <:Union{MOI.GreaterThan,MOI.LessThan,MOI.EqualTo}, - }, - }, -) + ::Type{ComplementsWithSetType{S}}, +) where {S<:Union{MOI.GreaterThan,MOI.LessThan,MOI.EqualTo}} return true end diff --git a/src/Bridges/FlipSignBridge.jl b/src/Bridges/FlipSignBridge.jl index 03cde16..975b7a3 100644 --- a/src/Bridges/FlipSignBridge.jl +++ b/src/Bridges/FlipSignBridge.jl @@ -38,18 +38,13 @@ struct FlipSignBridge{T,F,S1,S2,G} <: MOI.Bridges.Constraint.AbstractBridge original_func::G end -function _flip_set_type(::Type{MOI.Nonnegatives}) - return MOI.Nonpositives -end -function _flip_set_type(::Type{MOI.Nonpositives}) - return MOI.Nonnegatives -end -function _flip_set_type(::Type{MOI.GreaterThan{T}}) where {T} - return MOI.LessThan{T} -end -function _flip_set_type(::Type{MOI.LessThan{T}}) where {T} - return MOI.GreaterThan{T} -end +_flip_set_type(::Type{MOI.Nonnegatives}) = MOI.Nonpositives + +_flip_set_type(::Type{MOI.Nonpositives}) = MOI.Nonnegatives + +_flip_set_type(::Type{MOI.GreaterThan{T}}) where {T} = MOI.LessThan{T} + +_flip_set_type(::Type{MOI.LessThan{T}}) where {T} = MOI.GreaterThan{T} const _FlippableSets = Union{MOI.Nonnegatives,MOI.Nonpositives,MOI.GreaterThan,MOI.LessThan} diff --git a/src/Bridges/NonlinearBridge.jl b/src/Bridges/NonlinearBridge.jl index 7df4ed2..a499dac 100644 --- a/src/Bridges/NonlinearBridge.jl +++ b/src/Bridges/NonlinearBridge.jl @@ -155,6 +155,7 @@ function _complementarity_bounds( ) where {T} return (zero(T), T(Inf)) end + function _complementarity_bounds( ::Type{MOI.Nonpositives}, model, @@ -163,6 +164,7 @@ function _complementarity_bounds( ) where {T} return (T(-Inf), zero(T)) end + function _complementarity_bounds( ::Type{MOI.Zeros}, model, @@ -171,6 +173,7 @@ function _complementarity_bounds( ) where {T} return (zero(T), zero(T)) end + function _complementarity_bounds( ::Type{<:MOI.GreaterThan}, model, @@ -179,6 +182,7 @@ function _complementarity_bounds( ) where {T} return (MOI.Utilities.get_bounds(model, T, x2)[1], T(Inf)) end + function _complementarity_bounds( ::Type{<:MOI.LessThan}, model, @@ -187,6 +191,7 @@ function _complementarity_bounds( ) where {T} return (T(-Inf), MOI.Utilities.get_bounds(model, T, x2)[2]) end + function _complementarity_bounds( ::Type{<:MOI.Interval}, model, @@ -197,11 +202,15 @@ function _complementarity_bounds( end """ - reformulate_as_nonlinear_program!(model, relaxation, fun, set::ComplementsWithSetType{S}) + reformulate_as_nonlinear_program!( + model, + relaxation, + fun, + set::ComplementsWithSetType{S}, + ) Reformulate complementarity constraints as a nonlinear program using the given relaxation. The set type `S` determines which bound case to use. - """ function reformulate_as_nonlinear_program!( model::MOI.ModelLike, @@ -334,7 +343,6 @@ For `epsilon ≥ 0`, the complementarity constraint `0 ≤ a ⟂ b ≥ 0` is ref 0 ≤ b a + b - sqrt((a + b)^2 + epsilon) ≤ 0 ``` - """ struct FischerBurmeisterRelaxation{T} <: AbstractComplementarityRelaxation epsilon::T @@ -439,13 +447,27 @@ For `epsilon ≥ 0`, the complementarity constraint `0 ≤ a ⟂ b ≥ 0` is ref ``` a . b ≤ epsilon^2 (a + epsilon) . (b + epsilon) ≥ epsilon^2 - ``` """ struct LiuFukushimaRelaxation{T} <: AbstractComplementarityRelaxation epsilon::T end +function _remove_bounds!(model::MOI.ModelLike, x::MOI.VariableIndex) + for cidx in [ + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}(x.value), + MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(x.value), + MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}( + x.value, + ), + ] + if MOI.is_valid(model, cidx) + MOI.delete(model, cidx) + end + end + return +end + function _relax_complementarity_lower_bound!( model::MOI.ModelLike, relaxation::LiuFukushimaRelaxation, @@ -528,7 +550,6 @@ with the function `ϕ`: -0.5 ((a -epsilon)^2 + (b - epsilon)^2) otherwise ``` - """ struct KanzowSchwarzRelaxation{T} <: AbstractComplementarityRelaxation epsilon::T diff --git a/src/Bridges/VerticalBridge.jl b/src/Bridges/VerticalBridge.jl index 203ea1a..bbcb1c2 100644 --- a/src/Bridges/VerticalBridge.jl +++ b/src/Bridges/VerticalBridge.jl @@ -187,6 +187,7 @@ function _is_single_variable(func::MOI.ScalarAffineFunction) func.terms[1].coefficient == 1.0 && iszero(func.constant) end + function _is_single_variable(func::MOI.ScalarQuadraticFunction) return ( length(func.quadratic_terms) == 0 && @@ -195,13 +196,17 @@ function _is_single_variable(func::MOI.ScalarQuadraticFunction) iszero(func.constant) ) end + function _is_single_variable(func::MOI.ScalarNonlinearFunction) return func.head == :+ && length(func.args) == 1 && isa(func.args[1], MOI.VariableIndex) end + _get_variable(func::MOI.ScalarAffineFunction) = func.terms[1].variable + _get_variable(func::MOI.ScalarQuadraticFunction) = func.affine_terms[1].variable + _get_variable(func::MOI.ScalarNonlinearFunction) = func.args[1] # TODO: add support for ScalarNonlinearTerm @@ -211,10 +216,8 @@ function _parse_complementarity_constraint( ) exprs = MOI.Utilities.scalarize(fun) @assert length(exprs) == 2*n_comp - cc_lhs = MOI.AbstractScalarFunction[] cc_rhs = MOI.VariableIndex[] - for i in 1:n_comp # Parse LHS t1 = exprs[i] @@ -224,7 +227,6 @@ function _parse_complementarity_constraint( else push!(cc_lhs, t1) end - # Parse RHS isvar2 = _is_single_variable(t2) if !isvar2 @@ -237,7 +239,6 @@ function _parse_complementarity_constraint( end push!(cc_rhs, _get_variable(t2)) end - return cc_lhs, cc_rhs end @@ -250,7 +251,6 @@ expressions are rewritten with a slack. `T` is the coefficient type used for the generated equality constraints. Once reformulated, the complementarity constraints involve only single variables. - """ function reformulate_to_vertical!( model::MOI.ModelLike, diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index e00f918..473c752 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -10,6 +10,7 @@ mutable struct Optimizer{T,O<:MOI.ModelLike} <: constraint_map::MOI.Bridges.Constraint.Map con_to_name::Dict{MOI.ConstraintIndex,String} name_to_con::Union{Dict{String,MOI.ConstraintIndex},Nothing} + function Optimizer{T}(model::MOI.ModelLike) where {T} return new{T,typeof(model)}( model, @@ -45,12 +46,14 @@ MOI.Bridges.is_bridged(::Optimizer, ::Type{<:MOI.AbstractSet}) = false # Complements and ComplementsWithSetType are bridged MOI.Bridges.is_bridged(::Optimizer, ::Type{MOI.Complements}) = true + function MOI.Bridges.supports_bridging_constrained_variable( ::Optimizer, ::Type{MOI.Complements}, ) return true end + function MOI.Bridges.bridge_type( ::Optimizer{T}, ::Type{MOI.Complements}, @@ -59,6 +62,7 @@ function MOI.Bridges.bridge_type( end MOI.Bridges.is_bridged(::Optimizer, ::Type{<:ComplementsWithSetType}) = true + function MOI.Bridges.supports_bridging_constrained_variable( ::Optimizer, ::Type{<:ComplementsWithSetType}, @@ -86,6 +90,7 @@ function MOI.Bridges.is_bridged( ) return true end + function MOI.Bridges.supports_bridging_constraint( ::Optimizer, ::Type{<:MOI.AbstractVectorFunction}, @@ -93,6 +98,7 @@ function MOI.Bridges.supports_bridging_constraint( ) return true end + function MOI.Bridges.bridge_type( ::Optimizer{T}, ::Type{<:MOI.AbstractVectorFunction}, @@ -118,6 +124,7 @@ function MOI.Bridges.is_bridged( ) return true end + function MOI.Bridges.supports_bridging_constraint( ::Optimizer, ::Type{<:MOI.AbstractVectorFunction}, @@ -251,8 +258,10 @@ function _additional_arguments( return (model.reformulation,) end -# TODO it would be nice if MOI was defining this `MOI.Bridges.additional_arguments` function and -# already had this implementation of `add_bridged_constraint` so that I don't have to reimplement it +# TODO(blegat): it would be nice if MOI was defining this +# `MOI.Bridges.additional_arguments` function and already had this +# implementation of `add_bridged_constraint` so that I don't have to reimplement +# it function MOI.Bridges.add_bridged_constraint(b::Optimizer, BridgeType, f, s) bridge = MOI.Bridges.Constraint.Constraint.bridge_constraint( BridgeType, @@ -261,7 +270,8 @@ function MOI.Bridges.add_bridged_constraint(b::Optimizer, BridgeType, f, s) s, _additional_arguments(b, BridgeType)..., ) - # The rest is copy-pasted from the default implementation of `add_bridged_constraint` in MOI + # The rest is copy-pasted from the default implementation of + # `add_bridged_constraint` in MOI ci = MOI.Bridges.Constraint.add_key_for_bridge( MOI.Bridges.Constraint.bridges(b)::MOI.Bridges.Constraint.Map, bridge, diff --git a/src/MathOptComplements.jl b/src/MathOptComplements.jl index ea156d7..f51bd5d 100644 --- a/src/MathOptComplements.jl +++ b/src/MathOptComplements.jl @@ -7,9 +7,92 @@ module MathOptComplements import MathOptInterface as MOI -include("utils.jl") -include("attributes.jl") -include("sets.jl") +""" + AbstractComplementarityRelaxation + +Abstract type to implement any complementarity function ``\\psi``. + +""" +abstract type AbstractComplementarityRelaxation end + +""" + DefaultComplementarityReformulation <: MOI.AbstractOptimizerAttribute + +Optimizer attribute that sets the default +[`AbstractComplementarityRelaxation`](@ref) used to reformulate all +complementarity constraints. + +This default is used for any constraint that does not have a constraint-specific +reformulation set via [`ComplementarityReformulation`](@ref). + +## Example + +```julia +julia> using JuMP, MathOptComplements + +julia> model = Model(); + +julia> set_attribute( + model, + MathOptComplements.DefaultComplementarityReformulation(), + MathOptComplements.ScholtesRelaxation(0.0), + ) +``` +""" +struct DefaultComplementarityReformulation <: MOI.AbstractOptimizerAttribute end + +""" + ComplementarityReformulation <: MOI.AbstractConstraintAttribute + +Constraint attribute that overrides the +[`AbstractComplementarityRelaxation`](@ref) for a specific complementarity + constraint. + +When set, this takes precedence over the model-wide default set via +[`DefaultComplementarityReformulation`](@ref). When not set, [`MOI.get`](@ref) +returns the model-wide default. + +## Example + +```julia +julia> using JuMP, MathOptComplements + +julia> model = Model(); + +julia> set_attribute( + model, + MathOptComplements.DefaultComplementarityReformulation(), + MathOptComplements.ScholtesRelaxation(0.0), + ) + +julia> c = @constraint(model, x ⟂ y); + +julia> set_attribute( + c, + MathOptComplements.ComplementarityReformulation(), + MathOptComplements.FischerBurmeisterRelaxation(1e-8), + ) +``` +""" +struct ComplementarityReformulation <: MOI.AbstractConstraintAttribute end + +""" + ComplementsWithSetType{S<:MOI.AbstractSet} <: MOI.AbstractVectorSet + +Complementarity constraint where each slack variable (second half of the vector) +is asserted to belong to set type `S`. For a constraint with `dimension = 2n`, +the `n` complementarity pairs are `(x[i], x[i+n])` for `i = 1, …, n`. + +`S` can be a scalar set type (`MOI.GreaterThan{T}`, `MOI.LessThan{T}`, +`MOI.EqualTo{T}`, `MOI.Interval{T}`) or a vector set type (`MOI.Nonnegatives`, +`MOI.Nonpositives`, `MOI.Zeros`). +""" +struct ComplementsWithSetType{S<:MOI.AbstractSet} <: MOI.AbstractVectorSet + dimension::Int +end + +MOI.dimension(set::ComplementsWithSetType) = set.dimension + include("Bridges/Bridges.jl") using .Bridges: diff --git a/src/attributes.jl b/src/attributes.jl deleted file mode 100644 index cf5f572..0000000 --- a/src/attributes.jl +++ /dev/null @@ -1,49 +0,0 @@ -# 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. - -""" - AbstractComplementarityRelaxation - -Abstract type to implement any complementarity function ``\\psi``. - -""" -abstract type AbstractComplementarityRelaxation end - -""" - DefaultComplementarityReformulation <: MOI.AbstractOptimizerAttribute - -Optimizer attribute that sets the default [`AbstractComplementarityRelaxation`](@ref) -used to reformulate all complementarity constraints. - -This default is used for any constraint that does not have a constraint-specific -reformulation set via [`ComplementarityReformulation`](@ref). - -## Example - -```julia -MOI.set(model, MathOptComplements.DefaultComplementarityReformulation(), MathOptComplements.ScholtesRelaxation(0.0)) -``` -""" -struct DefaultComplementarityReformulation <: MOI.AbstractOptimizerAttribute end - -""" - ComplementarityReformulation <: MOI.AbstractConstraintAttribute - -Constraint attribute that overrides the [`AbstractComplementarityRelaxation`](@ref) -for a specific complementarity constraint. - -When set, this takes precedence over the model-wide default set via -[`DefaultComplementarityReformulation`](@ref). When not set, [`MOI.get`](@ref) returns the -model-wide default. - -## Example - -```julia -MOI.set(model, MathOptComplements.DefaultComplementarityReformulation(), MathOptComplements.ScholtesRelaxation(0.0)) -c = @constraint(model, x ⟂ y) -MOI.set(model, MathOptComplements.ComplementarityReformulation(), c, MathOptComplements.FischerBurmeisterRelaxation(1e-8)) -``` -""" -struct ComplementarityReformulation <: MOI.AbstractConstraintAttribute end diff --git a/src/sets.jl b/src/sets.jl deleted file mode 100644 index a0aff53..0000000 --- a/src/sets.jl +++ /dev/null @@ -1,22 +0,0 @@ -# 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. - -""" - ComplementsWithSetType{S<:MOI.AbstractSet} <: MOI.AbstractVectorSet - -Complementarity constraint where each slack variable (second half of the vector) -is asserted to belong to set type `S`. For a constraint with `dimension = 2n`, -the `n` complementarity pairs are `(x[i], x[i+n])` for `i = 1, …, n`. - -`S` can be a scalar set type (`MOI.GreaterThan{T}`, `MOI.LessThan{T}`, -`MOI.EqualTo{T}`, `MOI.Interval{T}`) or a vector set type -(`MOI.Nonnegatives`, `MOI.Nonpositives`, `MOI.Zeros`). - -""" -struct ComplementsWithSetType{S<:MOI.AbstractSet} <: MOI.AbstractVectorSet - dimension::Int -end - -MOI.dimension(set::ComplementsWithSetType) = set.dimension diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index 62a419e..0000000 --- a/src/utils.jl +++ /dev/null @@ -1,19 +0,0 @@ -# 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. - -function _remove_bounds!(model::MOI.ModelLike, x::MOI.VariableIndex) - for cidx in [ - MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}(x.value), - MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(x.value), - MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}( - x.value, - ), - ] - if MOI.is_valid(model, cidx) - MOI.delete(model, cidx) - end - end - return -end