diff --git a/docs/src/submodules/Bridges/list_of_bridges.md b/docs/src/submodules/Bridges/list_of_bridges.md index b07afb8ad4..a83c5a6858 100644 --- a/docs/src/submodules/Bridges/list_of_bridges.md +++ b/docs/src/submodules/Bridges/list_of_bridges.md @@ -28,7 +28,10 @@ Bridges.Constraint.ScalarSlackBridge Bridges.Constraint.VectorSlackBridge Bridges.Constraint.ScalarFunctionizeBridge Bridges.Constraint.VectorFunctionizeBridge -Bridges.Constraint.ScalarQuadraticToScalarNonlinearBridge +Bridges.Constraint.ToScalarQuadraticBridge +Bridges.Constraint.ToVectorQuadraticBridge +Bridges.Constraint.ToScalarNonlinearBridge +Bridges.Constraint.FunctionConversionBridge Bridges.Constraint.SplitComplexEqualToBridge Bridges.Constraint.SplitComplexZerosBridge Bridges.Constraint.SplitHyperRectangleBridge diff --git a/docs/src/submodules/Bridges/reference.md b/docs/src/submodules/Bridges/reference.md index 7fa901c6de..44d9a1855e 100644 --- a/docs/src/submodules/Bridges/reference.md +++ b/docs/src/submodules/Bridges/reference.md @@ -20,6 +20,7 @@ get(::Bridges.AbstractBridge, ::NumberOfConstraints) get(::Bridges.AbstractBridge, ::ListOfConstraintIndices) Bridges.needs_final_touch Bridges.final_touch +Bridges.bridging_cost ``` ## Constraint bridge API @@ -35,6 +36,7 @@ Bridges.Constraint.add_all_bridges Bridges.Constraint.FlipSignBridge Bridges.Constraint.AbstractToIntervalBridge Bridges.Constraint.SetMapBridge +Bridges.Constraint.conversion_cost ``` ## Objective bridge API diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 0be042680f..3e14f12434 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -84,10 +84,9 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} MOI.Bridges.add_bridge(bridged_model, VectorSlackBridge{T}) MOI.Bridges.add_bridge(bridged_model, ScalarFunctionizeBridge{T}) MOI.Bridges.add_bridge(bridged_model, VectorFunctionizeBridge{T}) - MOI.Bridges.add_bridge( - bridged_model, - ScalarQuadraticToScalarNonlinearBridge{T}, - ) + MOI.Bridges.add_bridge(bridged_model, ToScalarQuadraticBridge{T}) + MOI.Bridges.add_bridge(bridged_model, ToVectorQuadraticBridge{T}) + MOI.Bridges.add_bridge(bridged_model, ToScalarNonlinearBridge{T}) MOI.Bridges.add_bridge(bridged_model, SplitHyperRectangleBridge{T}) MOI.Bridges.add_bridge(bridged_model, SplitIntervalBridge{T}) MOI.Bridges.add_bridge(bridged_model, SplitComplexEqualToBridge{T}) diff --git a/src/Bridges/Constraint/bridges/functionize.jl b/src/Bridges/Constraint/bridges/functionize.jl index ff98f04511..464d1653a4 100644 --- a/src/Bridges/Constraint/bridges/functionize.jl +++ b/src/Bridges/Constraint/bridges/functionize.jl @@ -72,6 +72,62 @@ function MOI.set( return end +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:AbstractFunctionConversionBridge}, +) + return Tuple{Type}[] +end + +function MOI.Bridges.added_constraint_types( + ::Type{<:AbstractFunctionConversionBridge{F,S}}, +) where {F,S} + return Tuple{Type,Type}[(F, S)] +end + +function MOI.get( + ::AbstractFunctionConversionBridge{F,S}, + ::MOI.NumberOfConstraints{F,S}, +)::Int64 where {F,S} + return 1 +end + +function MOI.get( + b::AbstractFunctionConversionBridge{F,S}, + ::MOI.ListOfConstraintIndices{F,S}, +) where {F,S} + return [b.constraint] +end + +function MOI.delete(model::MOI.ModelLike, c::AbstractFunctionConversionBridge) + MOI.delete(model, c.constraint) + return +end + +function MOI.set( + model::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::AbstractFunctionConversionBridge{F}, + f, +) where {F} + MOI.set(model, MOI.ConstraintFunction(), bridge.constraint, convert(F, f)) + return +end + +function MOI.delete( + model::MOI.ModelLike, + bridge::AbstractFunctionConversionBridge{F}, + i::MOI.Bridges.IndexInVector, +) where {F<:MOI.AbstractVectorFunction} + func = MOI.get(model, MOI.ConstraintFunction(), bridge.constraint) + idx = setdiff(1:MOI.output_dimension(func), i.value) + new_func = MOI.Utilities.eachscalar(func)[idx] + set = MOI.get(model, MOI.ConstraintSet(), bridge.constraint) + new_set = MOI.update_dimension(set, MOI.dimension(set) - 1) + MOI.delete(model, bridge.constraint) + bridge.constraint = MOI.add_constraint(model, new_func, new_set) + return +end + """ invariant_under_function_conversion(attr::MOI.AbstractConstraintAttribute) @@ -101,99 +157,172 @@ function invariant_under_function_conversion( end """ - ScalarFunctionizeBridge{T,S} <: Bridges.Constraint.AbstractBridge + FunctionConversionBridge{T,F,G,S} <: AbstractFunctionConversionBridge{G,S} -`ScalarFunctionizeBridge` implements the following reformulations: +`FunctionConversionBridge` implements the following reformulations: - * ``x \\in S`` into ``1x + 0 \\in S`` + * ``g(x) \\in S`` into ``f(x) \\in S`` + +for these pairs of functions: + + * [`MOI.ScalarAffineFunction`](@ref)` to [`MOI.ScalarQuadraticFunction`](@ref) + * [`MOI.ScalarQuadraticFunction`](@ref) to [`MOI.ScalarNonlinearFunction`](@ref) + * [`MOI.VectorAffineFunction`](@ref) to [`MOI.VectorQuadraticFunction`](@ref) ## Source node -`ScalarFunctionizeBridge` supports: +`FunctionConversionBridge` supports: - * [`MOI.VariableIndex`](@ref) in `S` + * `G` in `S` ## Target nodes -`ScalarFunctionizeBridge` creates: +`FunctionConversionBridge` creates: - * [`MOI.ScalarAffineFunction{T}`](@ref) in `S` + * `F` in `S` """ -struct ScalarFunctionizeBridge{T,S} <: - AbstractFunctionConversionBridge{MOI.ScalarAffineFunction{T},S} - constraint::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S} +mutable struct FunctionConversionBridge{T,F,G,S} <: + AbstractFunctionConversionBridge{F,S} + constraint::MOI.ConstraintIndex{F,S} end - -const ScalarFunctionize{T,OT<:MOI.ModelLike} = - SingleBridgeOptimizer{ScalarFunctionizeBridge{T},OT} +# The `struct` needs to be mutable if `F <: AbstractVectorFunction` +# in case one row is deleted. See `MOI.delete` above. function bridge_constraint( - ::Type{ScalarFunctionizeBridge{T,S}}, - model, - f::MOI.VariableIndex, + ::Type{FunctionConversionBridge{T,F,G,S}}, + model::MOI.ModelLike, + f::G, s::S, -) where {T,S} - constraint = MOI.add_constraint(model, MOI.ScalarAffineFunction{T}(f), s) - return ScalarFunctionizeBridge{T,S}(constraint) +) where {T,F,G,S} + ci = MOI.add_constraint(model, convert(F, f), s) + return FunctionConversionBridge{T,F,G,S}(ci) end -function MOI.supports_constraint( - ::Type{ScalarFunctionizeBridge{T}}, - ::Type{<:MOI.VariableIndex}, - ::Type{<:MOI.AbstractScalarSet}, -) where {T} - return true -end +""" + conversion_cost( + F::Type{<:MOI.AbstractFunction}, + G::Type{<:MOI.AbstractFunction}, + )::Float64 -function MOI.Bridges.added_constrained_variable_types( - ::Type{<:ScalarFunctionizeBridge}, +Return a `Float64` returning the cost of converting any function of type `G` +to a function of type `F` with `convert`. + +This cost is used to compute [`MOI.Bridges.bridging_cost`](@ref). + +The default cost is `Inf`, which means that +[`MOI.Bridges.Constraint.FunctionConversionBridge`](@ref) should not attempt the +conversion. +""" +function conversion_cost( + ::Type{<:MOI.AbstractFunction}, + ::Type{<:MOI.AbstractFunction}, ) - return Tuple{Type}[] + return Inf end -function MOI.Bridges.added_constraint_types( - ::Type{ScalarFunctionizeBridge{T,S}}, -) where {T,S} - return Tuple{Type,Type}[(MOI.ScalarAffineFunction{T}, S)] +function conversion_cost( + F::Type{<:MOI.AbstractVectorFunction}, + G::Type{<:MOI.AbstractVectorFunction}, +) + return conversion_cost( + MOI.Utilities.scalar_type(F), + MOI.Utilities.scalar_type(G), + ) end -function concrete_bridge_type( - ::Type{<:ScalarFunctionizeBridge{T}}, +function conversion_cost( + ::Type{<:MOI.ScalarAffineFunction}, ::Type{MOI.VariableIndex}, - S::Type{<:MOI.AbstractScalarSet}, +) + return 1.0 +end + +function conversion_cost( + ::Type{MOI.ScalarQuadraticFunction{T}}, + ::Type{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}}, ) where {T} - return ScalarFunctionizeBridge{T,S} + return 10.0 end -function MOI.get( - ::ScalarFunctionizeBridge{T,S}, - ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},S}, -)::Int64 where {T,S} - return 1 +function conversion_cost( + ::Type{MOI.ScalarNonlinearFunction}, + ::Type{ + <:Union{ + MOI.VariableIndex, + MOI.ScalarAffineFunction, + MOI.ScalarQuadraticFunction, + }, + }, +) + return 100.0 end -function MOI.get( - b::ScalarFunctionizeBridge{T,S}, - ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},S}, -) where {T,S} - return [b.constraint] +function MOI.supports_constraint( + ::Type{<:FunctionConversionBridge{T,F}}, + ::Type{G}, + ::Type{<:MOI.AbstractSet}, +) where {T,F,G<:MOI.AbstractFunction} + return isfinite(conversion_cost(F, G)) end -function MOI.delete(model::MOI.ModelLike, c::ScalarFunctionizeBridge) - MOI.delete(model, c.constraint) - return +function concrete_bridge_type( + ::Type{<:FunctionConversionBridge{T,F}}, + G::Type{<:MOI.AbstractFunction}, + S::Type{<:MOI.AbstractSet}, +) where {T,F} + return FunctionConversionBridge{T,F,G,S} +end + +function MOI.Bridges.bridging_cost( + ::Type{<:FunctionConversionBridge{T,F,G}}, +) where {T,F,G} + return conversion_cost(F, G) end function MOI.get( model::MOI.ModelLike, - attr::MOI.ConstraintFunction, - b::ScalarFunctionizeBridge, -) - return convert(MOI.VariableIndex, MOI.get(model, attr, b.constraint)) + ::MOI.ConstraintFunction, + b::FunctionConversionBridge{T,F,G}, +) where {T,F,G} + # TODO(odow): there's a bug _somewhere_ in call_in_context, which means that + # using CanonicalConstraintFunction here doesn't work. I got too confused + # trying to track it down, so I explicitly called canonical here instead. + # We need canonical, because the downstream bridges may have added + # additional terms that mean it can't be directly converted to G. + f = MOI.get(model, MOI.ConstraintFunction(), b.constraint) + return convert(G, MOI.Utilities.canonical(f)) end """ - VectorFunctionizeBridge{T,S} <: Bridges.Constraint.AbstractBridge + + ScalarFunctionizeBridge{T,S} = FunctionConversionBridge{T,MOI.ScalarAffineFunction{T},MOI.VariableIndex,S} + +`ScalarFunctionizeBridge` implements the following reformulations: + + * ``x \\in S`` into ``1x + 0 \\in S`` + +## Source node + +`ScalarFunctionizeBridge` supports: + + * [`MOI.VariableIndex`](@ref) in `S` + +## Target nodes + +`ScalarFunctionizeBridge` creates: + + * [`MOI.ScalarAffineFunction{T}`](@ref) in `S` +""" +const ScalarFunctionizeBridge{T,S} = + FunctionConversionBridge{T,MOI.ScalarAffineFunction{T},MOI.VariableIndex,S} + +const ScalarFunctionize{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ScalarFunctionizeBridge{T},OT} + +# VectorOfVariables -> VectorAffineFunction # Handled by VectorFunctionizeBridge + +""" + VectorFunctionizeBridge{T,S} = FunctionConversionBridge{T,MOI.VectorAffineFunction{T},S} `VectorFunctionizeBridge` implements the following reformulations: @@ -211,205 +340,102 @@ end * [`MOI.VectorAffineFunction{T}`](@ref) in `S` """ -mutable struct VectorFunctionizeBridge{T,S} <: - AbstractFunctionConversionBridge{MOI.VectorAffineFunction{T},S} - constraint::MOI.ConstraintIndex{MOI.VectorAffineFunction{T},S} -end +const VectorFunctionizeBridge{T,S} = FunctionConversionBridge{ + T, + MOI.VectorAffineFunction{T}, + MOI.VectorOfVariables, + S, +} const VectorFunctionize{T,OT<:MOI.ModelLike} = SingleBridgeOptimizer{VectorFunctionizeBridge{T},OT} -function bridge_constraint( - ::Type{VectorFunctionizeBridge{T,S}}, - model, - f::MOI.VectorOfVariables, - s::S, -) where {T,S} - constraint = MOI.add_constraint(model, MOI.VectorAffineFunction{T}(f), s) - return VectorFunctionizeBridge{T,S}(constraint) -end +# AbstractScalarFunction -> ScalarQuadraticFunction -function MOI.supports_constraint( - ::Type{VectorFunctionizeBridge{T}}, - ::Type{MOI.VectorOfVariables}, - ::Type{<:MOI.AbstractVectorSet}, -) where {T} - return true -end +""" + ToScalarQuadraticBridge{T,G,S} <: AbstractFunctionConversionBridge{G,S} -function MOI.Bridges.added_constrained_variable_types( - ::Type{<:VectorFunctionizeBridge}, -) - return Tuple{Type}[] -end +`ToScalarQuadraticBridge` implements the following reformulation: -function MOI.Bridges.added_constraint_types( - ::Type{VectorFunctionizeBridge{T,S}}, -) where {T,S} - return Tuple{Type,Type}[(MOI.VectorAffineFunction{T}, S)] -end + * ``g(x) \\in S`` into ``f(x) \\in S`` -function concrete_bridge_type( - ::Type{<:VectorFunctionizeBridge{T}}, - ::Type{<:MOI.AbstractVectorFunction}, - S::Type{<:MOI.AbstractVectorSet}, -) where {T} - return VectorFunctionizeBridge{T,S} -end +where `g` is an abstract scalar function and `f` is a +[`MOI.ScalarQuadraticFunction`](@ref). -function MOI.get( - ::VectorFunctionizeBridge{T,S}, - ::MOI.NumberOfConstraints{MOI.VectorAffineFunction{T},S}, -)::Int64 where {T,S} - return 1 -end +## Source node -function MOI.get( - b::VectorFunctionizeBridge{T,S}, - ::MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{T},S}, -) where {T,S} - return [b.constraint] -end +`ToScalarQuadraticBridge` supports: -function MOI.delete(model::MOI.ModelLike, bridge::VectorFunctionizeBridge) - MOI.delete(model, bridge.constraint) - return -end + * `G<:AbstractScalarFunction` in `S` -function MOI.delete( - model::MOI.ModelLike, - bridge::VectorFunctionizeBridge, - i::MOI.Bridges.IndexInVector, -) - func = MOI.get(model, MOI.ConstraintFunction(), bridge.constraint) - idx = setdiff(1:MOI.output_dimension(func), i.value) - new_func = MOI.Utilities.eachscalar(func)[idx] - set = MOI.get(model, MOI.ConstraintSet(), bridge.constraint) - new_set = MOI.update_dimension(set, MOI.dimension(set) - 1) - MOI.delete(model, bridge.constraint) - bridge.constraint = MOI.add_constraint(model, new_func, new_set) - return -end +## Target nodes -function MOI.set( - model::MOI.ModelLike, - ::MOI.ConstraintFunction, - bridge::VectorFunctionizeBridge{T}, - func::MOI.VectorOfVariables, -) where {T} - MOI.set( - model, - MOI.ConstraintFunction(), - bridge.constraint, - MOI.VectorAffineFunction{T}(func), - ) - return -end +`ToScalarQuadraticBridge` creates: -function MOI.get( - model::MOI.ModelLike, - attr::MOI.ConstraintFunction, - b::VectorFunctionizeBridge, -) - f = MOI.get(model, attr, b.constraint) - return MOI.Utilities.convert_approx(MOI.VectorOfVariables, f) -end + * [`MOI.ScalarQuadraticFunction`](@ref) in `S` +""" +const ToScalarQuadraticBridge{T,G,S} = + FunctionConversionBridge{T,MOI.ScalarQuadraticFunction{T},G,S} + +const ToScalarQuadratic{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ToScalarQuadraticBridge{T},OT} + +# AbstractVectorFunction -> VectorQuadraticFunction """ - ScalarQuadraticToScalarNonlinearBridge{T,S} <: Bridges.Constraint.AbstractBridge + ToVectorQuadraticBridge{T,G,S} <: AbstractFunctionConversionBridge{G,S} -`ScalarQuadraticToScalarNonlinearBridge` implements the following reformulations: +`ToVectorQuadraticBridge` implements the following reformulation: - * ``f(x) \\in S`` into ``g(x) \\in S`` + * ``g(x) \\in S`` into ``f(x) \\in S`` -where `f` is a [`MOI.ScalarQuadraticFunction`](@ref) and `g` is a -[`MOI.ScalarNonlinearFunction{T}`](@ref). +where `g` is an abstract vector function and `f` is a +[`MOI.VectorQuadraticFunction`](@ref). ## Source node -`ScalarQuadraticToScalarNonlinearBridge` supports: +`ToVectorQuadraticBridge` supports: - * [`MOI.ScalarQuadraticFunction`](@ref) in `S` + * `G<:AbstractVectorFunction` in `S` ## Target nodes -`ScalarQuadraticToScalarNonlinearBridge` creates: +`ToVectorQuadraticBridge` creates: - * [`MOI.ScalarNonlinearFunction{T}`](@ref) in `S` + * [`MOI.VectorQuadraticFunction`](@ref) in `S` """ -struct ScalarQuadraticToScalarNonlinearBridge{T,S} <: - AbstractFunctionConversionBridge{MOI.ScalarNonlinearFunction,S} - constraint::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S} -end +const ToVectorQuadraticBridge{T,G,S} = + FunctionConversionBridge{T,MOI.VectorQuadraticFunction{T},G,S} -const ScalarQuadraticToScalarNonlinear{T,OT<:MOI.ModelLike} = - SingleBridgeOptimizer{ScalarQuadraticToScalarNonlinearBridge{T},OT} +const ToVectorQuadratic{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ToVectorQuadraticBridge{T},OT} -function bridge_constraint( - ::Type{ScalarQuadraticToScalarNonlinearBridge{T,S}}, - model::MOI.ModelLike, - f::MOI.ScalarQuadraticFunction{T}, - s::S, -) where {T,S} - ci = MOI.add_constraint(model, convert(MOI.ScalarNonlinearFunction, f), s) - return ScalarQuadraticToScalarNonlinearBridge{T,S}(ci) -end +# AbstractScalarFunction -> ScalarNonlinearFunction -function MOI.supports_constraint( - ::Type{ScalarQuadraticToScalarNonlinearBridge{T}}, - ::Type{MOI.ScalarQuadraticFunction{T}}, - ::Type{<:MOI.AbstractScalarSet}, -) where {T} - return true -end +""" + ToScalarNonlinearBridge{T,G,S} <: AbstractFunctionConversionBridge{G,S} -function MOI.Bridges.added_constrained_variable_types( - ::Type{<:ScalarQuadraticToScalarNonlinearBridge}, -) - return Tuple{Type}[] -end +`ToScalarNonlinearBridge` implements the following reformulation: -function MOI.Bridges.added_constraint_types( - ::Type{ScalarQuadraticToScalarNonlinearBridge{T,S}}, -) where {T,S} - return Tuple{Type,Type}[(MOI.ScalarNonlinearFunction, S)] -end + * ``g(x) \\in S`` into ``f(x) \\in S`` -function concrete_bridge_type( - ::Type{<:ScalarQuadraticToScalarNonlinearBridge{T}}, - ::Type{MOI.ScalarQuadraticFunction{T}}, - S::Type{<:MOI.AbstractScalarSet}, -) where {T} - return ScalarQuadraticToScalarNonlinearBridge{T,S} -end +where `g` is an abstract scalar function and `f` is a +[`MOI.ScalarNonlinearFunction`](@ref). -function MOI.get( - ::ScalarQuadraticToScalarNonlinearBridge{T,S}, - ::MOI.NumberOfConstraints{MOI.ScalarNonlinearFunction,S}, -)::Int64 where {T,S} - return 1 -end +## Source node -function MOI.get( - b::ScalarQuadraticToScalarNonlinearBridge{T,S}, - ::MOI.ListOfConstraintIndices{MOI.ScalarNonlinearFunction,S}, -) where {T,S} - return [b.constraint] -end +`ToScalarNonlinearBridge` supports: -function MOI.delete( - model::MOI.ModelLike, - c::ScalarQuadraticToScalarNonlinearBridge, -) - MOI.delete(model, c.constraint) - return -end + * `G<:AbstractScalarFunction` in `S` -function MOI.get( - model::MOI.ModelLike, - ::MOI.ConstraintFunction, - b::ScalarQuadraticToScalarNonlinearBridge{T}, -) where {T} - f = MOI.get(model, MOI.ConstraintFunction(), b.constraint) - return convert(MOI.ScalarQuadraticFunction{T}, f) -end +## Target nodes + +`ToScalarNonlinearBridge` creates: + + * [`MOI.ScalarNonlinearFunction`](@ref) in `S` +""" +const ToScalarNonlinearBridge{T,G,S} = + FunctionConversionBridge{T,MOI.ScalarNonlinearFunction,G,S} + +const ToScalarNonlinear{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ToScalarNonlinearBridge{T},OT} diff --git a/src/Bridges/bridge.jl b/src/Bridges/bridge.jl index 69a7a3abd4..47c951b13e 100644 --- a/src/Bridges/bridge.jl +++ b/src/Bridges/bridge.jl @@ -283,3 +283,40 @@ If you implement this method, you must also implement [`needs_final_touch`](@ref). """ function final_touch end + +""" + bridging_cost(BT::Type{<:AbstractBridge})::Float64 + +Return the cost of adding a bridge of type `BT`. + +The default implementation for any [`AbstractBridge`](@ref) returns `1.0`, so +this method should only be implemented for bridges returning a cost different +from `1.0`. + +## Example + +Since the [`Bridges.Constraint.FunctionConversionBridge`](@ref) converts +constraints from a given function type to a function type to a wider one, +we want it to have lower priority. + +For example, we want to prioritize bridging a +[`MOI.ScalarAffineFunction`](@ref)-in-[`MOI.LessThan`](@ref) constraint into a +[`MOI.VectorAffineFunction`](@ref)-in-[`MOI.Nonnegatives`](@ref) constraint +over bridging it to a [`MOI.ScalarQuadraticFunction`](@ref)-in-[`MOI.LessThan`](@ref) +constraint. + +For this reason, the [`Bridges.Constraint.FunctionConversionBridge`](@ref) is +given a cost of `10`: + +```jldoctest; setup=(import MathOptInterface as MOI) +julia> F = MOI.ScalarQuadraticFunction{Float64}; + +julia> G = MOI.ScalarAffineFunction{Float64}; + +julia> MOI.Bridges.bridging_cost( + MOI.Bridges.Constraint.FunctionConversionBridge{Float64,F,G}, + ) +10.0 +``` +""" +bridging_cost(::Type{<:AbstractBridge}) = 1.0 diff --git a/src/Bridges/debug.jl b/src/Bridges/debug.jl index 584b936c40..b27e1c9890 100644 --- a/src/Bridges/debug.jl +++ b/src/Bridges/debug.jl @@ -53,12 +53,14 @@ function print_node_info( if iszero(index) || (node isa VariableNode && !is_variable_edge_best(b.graph, node)) @assert node isa VariableNode + dist = isinteger(d) ? round(Int, d) : d println( io, - " supported (distance $d) by adding free variables and then constrain them, see ($(b.graph.variable_constraint_node[node.index].index)).", + " supported (distance $dist) by adding free variables and then constrain them, see ($(b.graph.variable_constraint_node[node.index].index)).", ) else - print(io, " bridged (distance $d) by ") + dist = isinteger(d) ? round(Int, d) : d + print(io, " bridged (distance $dist) by ") MOI.Utilities.print_with_acronym( io, string(_bridge_type(b, node, index)), diff --git a/src/Bridges/graph.jl b/src/Bridges/graph.jl index fc8f48f025..93db56160e 100644 --- a/src/Bridges/graph.jl +++ b/src/Bridges/graph.jl @@ -4,7 +4,7 @@ # 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. -const INFINITY = typemax(Int) +const INFINITY = Inf const INVALID_NODE_INDEX = -1 abstract type AbstractNode end @@ -43,6 +43,7 @@ abstract type AbstractEdge end bridge_index::Int, added_variables::Vector{VariableNode}, added_constraints::Vector{ConstraintNode}, + cost::Float64 = 1.0, ) Return a new datastructure representing an edge in [`Graph`](@ref) that starts @@ -52,6 +53,16 @@ struct Edge <: AbstractEdge bridge_index::Int added_variables::Vector{VariableNode} added_constraints::Vector{ConstraintNode} + cost::Float64 + # TODO remove in MOI v2 + function Edge( + bridge_index::Int, + added_variables::Vector{VariableNode}, + added_constraints::Vector{ConstraintNode}, + cost::Float64 = 1.0, + ) + return new(bridge_index, added_variables, added_constraints, cost) + end end """ @@ -69,6 +80,23 @@ struct ObjectiveEdge <: AbstractEdge added_variables::Vector{VariableNode} added_constraints::Vector{ConstraintNode} added_objective::ObjectiveNode + cost::Float64 + # TODO remove in MOI v2 + function ObjectiveEdge( + bridge_index::Int, + added_variables::Vector{VariableNode}, + added_constraints::Vector{ConstraintNode}, + added_objective::ObjectiveNode, + cost::Float64 = 1.0, + ) + return new( + bridge_index, + added_variables, + added_constraints, + added_objective, + cost, + ) + end end """ @@ -110,20 +138,20 @@ mutable struct Graph variable_edges::Vector{Vector{Edge}} variable_constraint_node::Vector{ConstraintNode} variable_constraint_cost::Vector{Int} - # variable node index -> Number of bridges that need to be used - variable_dist::Vector{Int} + # variable node index -> Sum of costs of bridges that need to be used + variable_dist::Vector{Float64} # variable node index -> Index of bridge to be used variable_best::Vector{Int} variable_last_correct::Int constraint_edges::Vector{Vector{Edge}} - # constraint node index -> Number of bridges that need to be used - constraint_dist::Vector{Int} + # constraint node index -> Sum of costs of bridges that need to be used + constraint_dist::Vector{Float64} # constraint node index -> Index of bridge to be used constraint_best::Vector{Int} constraint_last_correct::Int objective_edges::Vector{Vector{ObjectiveEdge}} - # objective node index -> Number of bridges that need to be used - objective_dist::Vector{Int} + # objective node index -> Sum of costs of bridges that need to be used + objective_dist::Vector{Float64} # objective node index -> Index of bridge to be used objective_best::Vector{Int} objective_last_correct::Int @@ -133,15 +161,15 @@ mutable struct Graph Vector{Edge}[], ConstraintNode[], Int[], - Int[], + Float64[], Int[], 0, Vector{Edge}[], - Int[], + Float64[], Int[], 0, Vector{ObjectiveEdge}[], - Int[], + Float64[], Int[], 0, ) @@ -269,8 +297,7 @@ end function bridging_cost(graph::Graph, node::AbstractNode) _compute_bellman_ford(graph) - dist = _dist(graph, node) - return dist == INFINITY ? Inf : float(dist) + return _dist(graph, node) end """ @@ -308,16 +335,16 @@ end function _updated_dist( graph::Graph, - current::Int, + current::Float64, edges::Vector{<:AbstractEdge}, ) bridge_index = 0 for edge in edges - cost = _dist(graph, edge) - if cost == INFINITY + dist = _dist(graph, edge) + if dist == INFINITY continue end - dist = 1 + cost + dist += edge.cost if dist < current current = dist bridge_index = edge.bridge_index diff --git a/src/Bridges/lazy_bridge_optimizer.jl b/src/Bridges/lazy_bridge_optimizer.jl index f6538c5dec..daac29de41 100644 --- a/src/Bridges/lazy_bridge_optimizer.jl +++ b/src/Bridges/lazy_bridge_optimizer.jl @@ -152,7 +152,12 @@ Return the `Edge` or `ObjectiveEdge` in the hyper-graph associated with the bridge `BT`, where `index` is the index of `BT` in the list of bridges. """ function _edge(b::LazyBridgeOptimizer, index::Int, BT::Type{<:AbstractBridge}) - return Edge(index, _variable_nodes(b, BT), _constraint_nodes(b, BT)) + return Edge( + index, + _variable_nodes(b, BT), + _constraint_nodes(b, BT), + bridging_cost(BT), + ) end # Method for objective bridges because they produce ObjectiveEdge. @@ -166,6 +171,7 @@ function _edge( _variable_nodes(b, BT), _constraint_nodes(b, BT), node(b, set_objective_function_type(BT)), + bridging_cost(BT), ) end diff --git a/src/functions.jl b/src/functions.jl index 0b27cfd3f1..a99c4a3fa4 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -1285,6 +1285,16 @@ function Base.convert( return VectorAffineFunction{T}(terms, zeros(T, length(terms))) end +function Base.convert( + ::Type{VectorAffineFunction{T}}, + f::VectorQuadraticFunction{T}, +) where {T} + if any(!iszero(t.scalar_term.coefficient) for t in f.quadratic_terms) + throw(InexactError(:convert, VectorAffineFunction{T}, f)) + end + return VectorAffineFunction{T}(f.affine_terms, f.constants) +end + # VectorQuadraticFunction function Base.convert( @@ -1324,6 +1334,17 @@ function Base.convert( ) end +function Base.convert( + ::Type{VectorQuadraticFunction{T}}, + f::VectorAffineFunction{T}, +) where {T} + return VectorQuadraticFunction{T}( + VectorQuadraticTerm{T}[], + f.terms, + f.constants, + ) +end + function Base.convert( ::Type{VectorQuadraticTerm{T}}, f::VectorQuadraticTerm, diff --git a/test/Bridges/Constraint/functionize.jl b/test/Bridges/Constraint/functionize.jl index e99dfe0b84..d058a76bf8 100644 --- a/test/Bridges/Constraint/functionize.jl +++ b/test/Bridges/Constraint/functionize.jl @@ -293,9 +293,31 @@ function test_runtests() return end -function test_scalar_quadratic_to_nonlinear() +function test_canonical_constraint_function() + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Bridges.Constraint.ScalarFunctionize{Float64}(inner) + x = MOI.add_variable(model) + ci = MOI.add_constraint(model, x, MOI.GreaterThan(0.0)) + @test MOI.get(model, MOI.CanonicalConstraintFunction(), ci) ≈ x + return +end + +function test_FunctionConversionBridge() + # ScalarAffineFunction -> ScalarQuadraticFunction MOI.Bridges.runtests( - MOI.Bridges.Constraint.ScalarQuadraticToScalarNonlinearBridge, + MOI.Bridges.Constraint.ToScalarQuadraticBridge, + """ + variables: x + 1.0 * x in ZeroOne() + """, + """ + variables: x + 0.0 * x * x + 1.0 * x in ZeroOne() + """, + ) + # ScalarQuadraticFunction -> ScalarNonlinearFunction + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ToScalarNonlinearBridge, """ variables: x, y 1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0 >= 1.0 @@ -305,6 +327,18 @@ function test_scalar_quadratic_to_nonlinear() ScalarNonlinearFunction(1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0) >= 1.0 """, ) + # VectorAffineFunction -> VectorQuadraticFunction + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ToVectorQuadraticBridge, + """ + variables: t, x + [1.0 * t, 1.0 * x] in SecondOrderCone(2) + """, + """ + variables: t, x + [0.0 * t * t + 1.0 * t, 0.0 * t * t + 1.0 * x] in SecondOrderCone(2) + """, + ) return end diff --git a/test/Bridges/lazy_bridge_optimizer.jl b/test/Bridges/lazy_bridge_optimizer.jl index fcf12ee52f..319b3742f8 100644 --- a/test/Bridges/lazy_bridge_optimizer.jl +++ b/test/Bridges/lazy_bridge_optimizer.jl @@ -1316,37 +1316,18 @@ function _test_continuous_conic_with_NoVariableModel(T) @test !MOI.Bridges.is_variable_bridged(bridged, cy) @test MOI.Bridges.bridge(bridged, cy) isa MOI.Bridges.Constraint.RSOCtoSOCBridge{T} - @test sprint(MOI.Bridges.print_graph, bridged) == - MOI.Utilities.replace_acronym( - """ -Bridge graph with 8 variable nodes, 17 constraint nodes and 0 objective nodes. - [1] constrained variables in `MOI.NormCone` are not supported - [2] constrained variables in `MOI.PowerCone{$T}` are not supported - [3] constrained variables in `MOI.RotatedSecondOrderCone` are supported (distance 2) by adding free variables and then constrain them, see (7). - [4] constrained variables in `MOI.PositiveSemidefiniteConeTriangle` are not supported - [5] constrained variables in `MOI.ScaledPositiveSemidefiniteConeTriangle` are not supported - [6] constrained variables in `MOI.SecondOrderCone` are supported (distance 2) by adding free variables and then constrain them, see (1). - [7] constrained variables in `MOI.Nonnegatives` are supported (distance 2) by adding free variables and then constrain them, see (12). - [8] constrained variables in `MOI.Interval{$T}` are supported (distance 3) by adding free variables and then constrain them, see (14). - (1) `MOI.VectorOfVariables`-in-`MOI.SecondOrderCone` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.VectorFunctionizeBridge{T,MOI.SecondOrderCone}). - (2) `MOI.VectorOfVariables`-in-`MOI.NormCone` constraints are not supported - (3) `MOI.VectorAffineFunction{$T}`-in-`MOI.NormCone` constraints are not supported - (4) `MOI.VectorAffineFunction{$T}`-in-`MOI.PowerCone{$T}` constraints are not supported - (5) `MOI.VectorOfVariables`-in-`MOI.PowerCone{$T}` constraints are not supported - (6) `MOI.VectorAffineFunction{$T}`-in-`MOI.RotatedSecondOrderCone` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.RSOCtoSOCBridge{T,MOI.VectorAffineFunction{T},MOI.VectorAffineFunction{T}}). - (7) `MOI.VectorOfVariables`-in-`MOI.RotatedSecondOrderCone` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.RSOCtoSOCBridge{T,MOI.VectorAffineFunction{T},MOI.VectorOfVariables}). - (8) `MOI.VectorAffineFunction{$T}`-in-`MOI.PositiveSemidefiniteConeTriangle` constraints are not supported - (9) `MOI.VectorOfVariables`-in-`MOI.PositiveSemidefiniteConeTriangle` constraints are not supported - (10) `MOI.VectorAffineFunction{$T}`-in-`MOI.ScaledPositiveSemidefiniteConeTriangle` constraints are not supported - (11) `MOI.VectorOfVariables`-in-`MOI.ScaledPositiveSemidefiniteConeTriangle` constraints are not supported - (12) `MOI.VectorOfVariables`-in-`MOI.Nonnegatives` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.NonnegToNonposBridge{T,MOI.VectorAffineFunction{T},MOI.VectorOfVariables}). - (13) `MOI.VariableIndex`-in-`MOI.GreaterThan{$T}` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.GreaterToLessBridge{T,MOI.ScalarAffineFunction{T},MOI.VariableIndex}). - (14) `MOI.VariableIndex`-in-`MOI.Interval{$T}` constraints are bridged (distance 2) by $(MOI.Bridges.Constraint.ScalarFunctionizeBridge{T,MOI.Interval{T}}). - (15) `MOI.ScalarAffineFunction{$T}`-in-`MOI.Interval{$T}` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.SplitIntervalBridge{T,MOI.ScalarAffineFunction{T},MOI.Interval{T},MOI.GreaterThan{T},MOI.LessThan{T}}). - (16) `MOI.VariableIndex`-in-`MOI.LessThan{$T}` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.LessToGreaterBridge{T,MOI.ScalarAffineFunction{T},MOI.VariableIndex}). - (17) `MOI.VariableIndex`-in-`MOI.EqualTo{$T}` constraints are bridged (distance 1) by $(MOI.Bridges.Constraint.VectorizeBridge{T,MOI.VectorAffineFunction{T},MOI.Zeros,MOI.VariableIndex}). -""", - ) + graph = sprint(MOI.Bridges.print_graph, bridged) + # The contents of `graph` can very as new bridges are added. Test that it + # prints something, and that key features are present. + # If this test fails in future, run `print(graph)` and update as necessary. + for needle in ( + "Bridge graph with ", + "constrained variables in `MOI.NormCone` are not supported", + "`MOI.VectorOfVariables`-in-`MOI.SecondOrderCone` constraints are bridged", + "`MOI.VectorOfVariables`-in-`MOI.ScaledPositiveSemidefiniteConeTriangle` constraints are not supported", + ) + @test occursin(needle, graph) + end return end @@ -2066,6 +2047,95 @@ function test_hermitian(T = Float64) end end +MOI.Utilities.@model( + Model2235, + (), + (MOI.LessThan,), + (MOI.Nonnegatives, MOI.RotatedSecondOrderCone), + (), + (), + (MOI.ScalarQuadraticFunction,), + (), + (MOI.VectorAffineFunction,), +) + +function MOI.supports_constraint( + ::Model2235, + ::Type{MOI.VariableIndex}, + ::Type{<:Union{MOI.LessThan,MOI.GreaterThan,MOI.Interval,MOI.EqualTo}}, +) + return false +end + +function test_ToScalarQuadraticBridge_used() + F, S = MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64} + inner = Model2235{Float64}() + # `FunctionConversionBridge{T,MOI.ScalarQuadraticFunction{T}}` bridge + # to a supported constraint in 1 bridge but it has a higher bridging cost + # This tests that the bridging cost is taken into account. + model = MOI.Bridges.full_bridge_optimizer(inner, Float64) + @test MOI.Bridges.bridging_cost(model, F, S) == 2.0 + @test MOI.Bridges.bridge_type(model, F, S) <: + MOI.Bridges.Constraint.LessToGreaterBridge{Float64} + MOI.Bridges.remove_bridge( + model, + MOI.Bridges.Constraint.LessToGreaterBridge{Float64}, + ) + @test MOI.Bridges.bridging_cost(model, F, S) == 2.0 + @test MOI.Bridges.bridge_type(model, F, S) <: + MOI.Bridges.Constraint.VectorizeBridge{Float64} + MOI.Bridges.remove_bridge( + model, + MOI.Bridges.Constraint.VectorizeBridge{Float64}, + ) + @test MOI.Bridges.bridging_cost(model, F, S) == 10.0 + @test MOI.Bridges.bridge_type(model, F, S) <: + MOI.Bridges.Constraint.ToScalarQuadraticBridge{Float64} + x = MOI.add_variable(model) + MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)) + @test MOI.get(inner, MOI.ListOfConstraintTypesPresent()) == + [(MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64})] + return +end + +function test_ToScalarQuadraticBridge_not_used() + F, S = MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64} + inner = Model2235{Float64}() + model = MOI.Bridges.LazyBridgeOptimizer(inner) + @test MOI.Bridges.bridging_cost(model, F, S) == Inf + MOI.Bridges.add_bridge( + model, + MOI.Bridges.Constraint.ToScalarQuadraticBridge{Float64}, + ) + @test MOI.Bridges.bridging_cost(model, F, S) == 10.0 + MOI.Bridges.add_bridge( + model, + MOI.Bridges.Constraint.LessToGreaterBridge{Float64}, + ) + MOI.Bridges.add_bridge( + model, + MOI.Bridges.Constraint.VectorizeBridge{Float64}, + ) + @test MOI.Bridges.bridging_cost(model, F, S) == 2.0 + x = MOI.add_variable(model) + MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)) + @test MOI.get(inner, MOI.ListOfConstraintTypesPresent()) == + [(MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives)] + return +end + +function test_ToScalarQuadraticBridge_variable_bounds() + inner = Model2235{Float64}() + model = MOI.Bridges.full_bridge_optimizer(inner, Float64) + x = MOI.add_variable(model) + MOI.add_constraint(model, x, MOI.LessThan(1.0)) + @test MOI.get(inner, MOI.ListOfConstraintTypesPresent()) == + [(MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives)] + F, S = MOI.VariableIndex, MOI.LessThan{Float64} + @test MOI.Bridges.bridging_cost(model, F, S) == 2.0 + return +end + end # module TestBridgesLazyBridgeOptimizer.runtests() diff --git a/test/functions.jl b/test/functions.jl index 61a281f0ce..0d3ac1693e 100644 --- a/test/functions.jl +++ b/test/functions.jl @@ -428,6 +428,33 @@ function test_convert_ScalarNonlinearFunction_ScalarQuadraticFunction() return end +function test_convert_VectorQuadraticFunction_VectorAffineFunction() + x = MOI.VariableIndex(1) + quadratic = MOI.VectorQuadraticTerm{Float64}[] + affine = [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x))] + constants = [2.0] + f = MOI.VectorQuadraticFunction(quadratic, affine, constants) + g = MOI.VectorAffineFunction(affine, constants) + @test convert(MOI.VectorAffineFunction{Float64}, f) ≈ g + push!( + quadratic, + MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(1.0, x, x)), + ) + @test_throws InexactError convert(MOI.VectorAffineFunction{Float64}, f) + return +end + +function test_convert_VectorAffineFunction_VectorQuadraticFunction() + x = MOI.VariableIndex(1) + quadratic = MOI.VectorQuadraticTerm{Float64}[] + affine = [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x))] + constants = [2.0] + f = MOI.VectorAffineFunction(affine, constants) + g = MOI.VectorQuadraticFunction(quadratic, affine, constants) + @test convert(MOI.VectorQuadraticFunction{Float64}, f) ≈ g + return +end + function runtests() for name in names(@__MODULE__; all = true) if startswith("$name", "test_")