diff --git a/docs/src/submodules/Bridges/list_of_bridges.md b/docs/src/submodules/Bridges/list_of_bridges.md index bed0fb6381..7fe2ce2c47 100644 --- a/docs/src/submodules/Bridges/list_of_bridges.md +++ b/docs/src/submodules/Bridges/list_of_bridges.md @@ -28,6 +28,7 @@ Bridges.Constraint.ScalarSlackBridge Bridges.Constraint.VectorSlackBridge Bridges.Constraint.ScalarFunctionizeBridge Bridges.Constraint.VectorFunctionizeBridge +Bridges.Constraint.ScalarQuadraticToScalarNonlinearBridge Bridges.Constraint.SplitComplexEqualToBridge Bridges.Constraint.SplitComplexZerosBridge Bridges.Constraint.SplitHyperRectangleBridge diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 174147c251..cbfc0fa7ad 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -83,6 +83,10 @@ 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, 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 4a8becda38..f6f8540e30 100644 --- a/src/Bridges/Constraint/bridges/functionize.jl +++ b/src/Bridges/Constraint/bridges/functionize.jl @@ -322,3 +322,102 @@ function MOI.get( f = MOI.get(model, attr, b.constraint) return MOI.Utilities.convert_approx(MOI.VectorOfVariables, f) end + +""" + ScalarQuadraticToScalarNonlinearBridge{T,S} <: Bridges.Constraint.AbstractBridge + +`ScalarQuadraticToScalarNonlinearBridge` implements the following reformulations: + + * ``f(x) \\in S`` into ``g(x) \\in S`` + +where `f` is a [`MOI.ScalarQuadraticFunction`](@ref) and `g` is a +[`MOI.ScalarNonlinearFunction{T}`](@ref). + +## Source node + +`ScalarQuadraticToScalarNonlinearBridge` supports: + + * [`MOI.ScalarQuadraticFunction`](@ref) in `S` + +## Target nodes + +`ScalarQuadraticToScalarNonlinearBridge` creates: + + * [`MOI.ScalarNonlinearFunction{T}`](@ref) in `S` +""" +struct ScalarQuadraticToScalarNonlinearBridge{T,S} <: + AbstractFunctionConversionBridge{MOI.ScalarNonlinearFunction,S} + constraint::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S} +end + +const ScalarQuadraticToScalarNonlinear{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ScalarQuadraticToScalarNonlinearBridge{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 + +function MOI.supports_constraint( + ::Type{ScalarQuadraticToScalarNonlinearBridge{T}}, + ::Type{MOI.ScalarQuadraticFunction{T}}, + ::Type{<:MOI.AbstractScalarSet}, +) where {T} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:ScalarQuadraticToScalarNonlinearBridge}, +) + return Tuple{Type}[] +end + +function MOI.Bridges.added_constraint_types( + ::Type{ScalarQuadraticToScalarNonlinearBridge{T,S}}, +) where {T,S} + return Tuple{Type,Type}[(MOI.ScalarNonlinearFunction, S)] +end + +function concrete_bridge_type( + ::Type{<:ScalarQuadraticToScalarNonlinearBridge{T}}, + ::Type{MOI.ScalarQuadraticFunction{T}}, + S::Type{<:MOI.AbstractScalarSet}, +) where {T} + return ScalarQuadraticToScalarNonlinearBridge{T,S} +end + +function MOI.get( + ::ScalarQuadraticToScalarNonlinearBridge{T,S}, + ::MOI.NumberOfConstraints{MOI.ScalarNonlinearFunction,S}, +)::Int64 where {T,S} + return 1 +end + +function MOI.get( + b::ScalarQuadraticToScalarNonlinearBridge{T,S}, + ::MOI.ListOfConstraintIndices{MOI.ScalarNonlinearFunction,S}, +) where {T,S} + return [b.constraint] +end + +function MOI.delete( + model::MOI.ModelLike, + c::ScalarQuadraticToScalarNonlinearBridge, +) + MOI.delete(model, c.constraint) + return +end + +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 diff --git a/src/functions.jl b/src/functions.jl index c966ae2274..adc9b0747d 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -900,6 +900,69 @@ function Base.convert( return ScalarAffineFunction{T}(f.affine_terms, f.constant) end +_order(x::Real, y::VariableIndex) = (x, y) +_order(x::VariableIndex, y::Real) = (y, x) +_order(x, y) = nothing + +function Base.convert( + ::Type{ScalarAffineTerm{T}}, + f::ScalarNonlinearFunction, +) where {T} + if f.head != :* || length(f.args) != 2 + throw(InexactError(:convert, ScalarAffineTerm, f)) + end + ret = _order(f.args[1], f.args[2]) + if ret === nothing + throw(InexactError(:convert, ScalarAffineTerm, f)) + end + return ScalarAffineTerm(convert(T, ret[1]), ret[2]) +end + +function _add_to_function( + f::ScalarAffineFunction{T}, + arg::Union{Real,VariableIndex,ScalarAffineFunction}, +) where {T} + return Utilities.operate!(+, T, f, arg) +end + +function _add_to_function( + f::ScalarAffineFunction{T}, + arg::ScalarNonlinearFunction, +) where {T} + if arg.head == :* && length(arg.args) == 2 + push!(f.terms, convert(ScalarAffineTerm{T}, arg)) + else + _add_to_function(f, convert(ScalarAffineFunction{T}, arg)) + end + return f +end + +_add_to_function(::ScalarAffineFunction, ::Any) = nothing + +# This is a very rough-and-ready conversion function that only works for very +# basic expressions, such as those created by +# `convert(ScalarNonlinearFunction, f)`. +function Base.convert( + ::Type{ScalarAffineFunction{T}}, + f::ScalarNonlinearFunction, +) where {T} + if f.head == :* && length(f.args) == 2 + term = convert(ScalarAffineTerm{T}, f) + return ScalarAffineFunction{T}([term], zero(T)) + end + if f.head != :+ + throw(InexactError(:convert, ScalarAffineFunction{T}, f)) + end + output = ScalarAffineFunction{T}(ScalarAffineTerm{T}[], zero(T)) + for arg in f.args + output = _add_to_function(output, arg) + if output === nothing + throw(InexactError(:convert, ScalarAffineFunction{T}, f)) + end + end + return output +end + # ScalarQuadraticFunction function Base.convert(::Type{ScalarQuadraticFunction{T}}, α::T) where {T} @@ -949,6 +1012,88 @@ function Base.convert( ) end +_order(x::Real, y::VariableIndex, z::VariableIndex) = (x, y, z) +_order(x::VariableIndex, y::Real, z::VariableIndex) = (y, x, z) +_order(x::VariableIndex, y::VariableIndex, z::Real) = (z, x, y) +_order(x, y, z) = nothing + +function Base.convert( + ::Type{ScalarQuadraticTerm{T}}, + f::ScalarNonlinearFunction, +) where {T} + if f.head != :* || length(f.args) != 3 + throw(InexactError(:convert, ScalarQuadraticTerm, f)) + end + ret = _order(f.args[1], f.args[2], f.args[3]) + if ret === nothing + throw(InexactError(:convert, ScalarQuadraticTerm, f)) + end + coef = convert(T, ret[1]) + if ret[2] == ret[3] + coef *= 2 + end + return ScalarQuadraticTerm(coef, ret[2], ret[3]) +end + +function _add_to_function( + f::ScalarQuadraticFunction{T}, + arg::Union{Real,VariableIndex,ScalarAffineFunction,ScalarQuadraticFunction}, +) where {T} + return Utilities.operate!(+, T, f, arg) +end + +function _add_to_function( + f::ScalarQuadraticFunction{T}, + arg::ScalarNonlinearFunction, +) where {T} + if arg.head == :* && length(arg.args) == 2 + push!(f.affine_terms, convert(ScalarAffineTerm{T}, arg)) + elseif arg.head == :* && length(arg.args) == 3 + push!(f.quadratic_terms, convert(ScalarQuadraticTerm{T}, arg)) + else + _add_to_function(f, convert(ScalarQuadraticFunction{T}, arg)) + end + return f +end + +# This is a very rough-and-ready conversion function that only works for very +# basic expressions, such as those created by +# `convert(ScalarNonlinearFunction, f)`. +function Base.convert( + ::Type{ScalarQuadraticFunction{T}}, + f::ScalarNonlinearFunction, +) where {T} + if f.head == :* + if length(f.args) == 2 + quad_terms = ScalarQuadraticTerm{T}[] + affine_terms = [convert(ScalarAffineTerm{T}, f)] + return ScalarQuadraticFunction{T}(quad_terms, affine_terms, zero(T)) + elseif length(f.args) == 3 + quad_terms = [convert(ScalarQuadraticTerm{T}, f)] + affine_terms = ScalarAffineTerm{T}[] + return ScalarQuadraticFunction{T}(quad_terms, affine_terms, zero(T)) + end + elseif f.head == :^ && length(f.args) == 2 && f.args[2] == 2 + return convert( + ScalarQuadraticFunction{T}, + ScalarNonlinearFunction(:*, Any[one(T), f.args[1], f.args[1]]), + ) + end + if f.head != :+ + throw(InexactError(:convert, ScalarQuadraticFunction{T}, f)) + end + output = ScalarQuadraticFunction( + ScalarQuadraticTerm{T}[], + ScalarAffineTerm{T}[], + zero(T), + ) + for arg in f.args + # Unlike ScalarAffineFunction, _add_to_function cannot return ::Nothing + output = _add_to_function(output, arg)::ScalarQuadraticFunction{T} + end + return output +end + # ScalarNonlinearFunction function Base.convert(::Type{ScalarNonlinearFunction}, term::ScalarAffineTerm) diff --git a/test/Bridges/Constraint/functionize.jl b/test/Bridges/Constraint/functionize.jl index 935915dce4..9b9bf70d2a 100644 --- a/test/Bridges/Constraint/functionize.jl +++ b/test/Bridges/Constraint/functionize.jl @@ -293,6 +293,21 @@ function test_runtests() return end +function test_scalar_quadratic_to_nonlinear() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ScalarQuadraticToScalarNonlinearBridge, + """ + variables: x, y + 1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0 >= 1.0 + """, + """ + variables: x, y + ScalarNonlinearFunction(1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0) >= 1.0 + """, + ) + return +end + end # module TestConstraintFunctionize.runtests() diff --git a/test/functions.jl b/test/functions.jl index 32127581a8..61a281f0ce 100644 --- a/test/functions.jl +++ b/test/functions.jl @@ -304,6 +304,130 @@ function test_ScalarNonlinearFunction_isapprox() return end +function test_convert_ScalarNonlinearFunction_ScalarAffineTerm() + x = MOI.VariableIndex(1) + f = MOI.ScalarAffineTerm(1.0, x) + g = MOI.ScalarNonlinearFunction(:*, Any[1.0, x]) + h = MOI.ScalarNonlinearFunction(:*, Any[x, 1.0]) + @test convert(MOI.ScalarNonlinearFunction, f) ≈ g + @test convert(MOI.ScalarAffineTerm{Float64}, g) == f + @test convert(MOI.ScalarAffineTerm{Float64}, h) == f + f = MOI.ScalarAffineTerm(1, x) + g = MOI.ScalarNonlinearFunction(:*, Any[1, x]) + h = MOI.ScalarNonlinearFunction(:*, Any[x, 1]) + @test convert(MOI.ScalarNonlinearFunction, f) ≈ g + @test convert(MOI.ScalarAffineTerm{Int}, h) == f + for f_error in ( + MOI.ScalarNonlinearFunction(:*, Any[1.0, x, 2.0]), + MOI.ScalarNonlinearFunction(:+, Any[1.0, x]), + MOI.ScalarNonlinearFunction(:*, Any[x, x]), + ) + @test_throws( + InexactError, + convert(MOI.ScalarAffineTerm{Float64}, f_error), + ) + end + return +end + +function test_convert_ScalarNonlinearFunction_ScalarAffineFunction() + x = MOI.VariableIndex(1) + y = MOI.VariableIndex(2) + for f in ( + 1.0 * x, + 1.0 * x + 2.0, + 1.0 * x + 1.0 * x + 2.0, + 1.0 * x + 2.0 * y + 2.0, + 2.0 * y + 2.0 + 2.0 * x, + ) + g = convert(MOI.ScalarNonlinearFunction, f) + @test convert(MOI.ScalarAffineFunction{Float64}, g) ≈ f + end + f_add = MOI.ScalarNonlinearFunction(:+, Any[1.0, x]) + for (f, g) in ( + MOI.ScalarNonlinearFunction(:*, Any[1.0, x]) => 1.0 * x, + MOI.ScalarNonlinearFunction(:+, Any[1.0, x]) => 1.0 + x, + MOI.ScalarNonlinearFunction(:+, Any[f_add, f_add]) => 2.0 + 2.0 * x, + ) + @test convert(MOI.ScalarAffineFunction{Float64}, f) ≈ g + end + for f_error in ( + MOI.ScalarNonlinearFunction(:/, Any[1.0, x]), + MOI.ScalarNonlinearFunction(:+, Any[1.0, 1.0*x*x]), + ) + @test_throws( + InexactError, + convert(MOI.ScalarAffineFunction{Float64}, f_error), + ) + end + return +end + +function test_convert_ScalarNonlinearFunction_ScalarQuadraticTerm() + x = MOI.VariableIndex(1) + y = MOI.VariableIndex(2) + f = MOI.ScalarQuadraticTerm(1, x, y) + g = MOI.ScalarNonlinearFunction(:*, Any[1, x, y]) + h = MOI.ScalarNonlinearFunction(:*, Any[x, 1, y]) + i = MOI.ScalarNonlinearFunction(:*, Any[x, y, 1]) + @test convert(MOI.ScalarNonlinearFunction, f) ≈ g + @test convert(MOI.ScalarQuadraticTerm{Int}, g) == f + @test convert(MOI.ScalarQuadraticTerm{Int}, h) == f + @test convert(MOI.ScalarQuadraticTerm{Int}, i) == f + f = MOI.ScalarQuadraticTerm(2.0, x, x) + g = MOI.ScalarNonlinearFunction(:*, Any[1.0, x, x]) + h = MOI.ScalarNonlinearFunction(:*, Any[x, 1.0, x]) + i = MOI.ScalarNonlinearFunction(:*, Any[x, x, 1.0]) + @test convert(MOI.ScalarNonlinearFunction, f) ≈ g + @test convert(MOI.ScalarQuadraticTerm{Float64}, g) == f + @test convert(MOI.ScalarQuadraticTerm{Float64}, h) == f + @test convert(MOI.ScalarQuadraticTerm{Float64}, i) == f + for f_error in ( + MOI.ScalarNonlinearFunction(:*, Any[1.0, x, 2.0]), + MOI.ScalarNonlinearFunction(:+, Any[1.0, x, x]), + MOI.ScalarNonlinearFunction(:*, Any[x, x]), + ) + @test_throws( + InexactError, + convert(MOI.ScalarQuadraticTerm{Float64}, f_error), + ) + end + return +end + +function test_convert_ScalarNonlinearFunction_ScalarQuadraticFunction() + x = MOI.VariableIndex(1) + y = MOI.VariableIndex(2) + for f in ( + 1.0 * x * x, + 1.0 * x * x + 2.0, + 1.0 * x * x + 1.0 * x * y + 2.0, + 1.0 * x + 2.0 * y * y + 2.0, + 2.0 * y + 2.0 + 2.0 * x * x, + ) + g = convert(MOI.ScalarNonlinearFunction, f) + @test convert(MOI.ScalarQuadraticFunction{Float64}, g) ≈ f + end + f_add = MOI.ScalarNonlinearFunction(:+, Any[1.0, x]) + for (f, g) in ( + MOI.ScalarNonlinearFunction(:*, Any[1.0, x]) => 0.0 * x * x + 1.0 * x, + MOI.ScalarNonlinearFunction(:*, Any[x, 1.0, x]) => 1.0 * x * x, + MOI.ScalarNonlinearFunction(:+, Any[1.0, x]) => 0.0 * x * x + 1.0 + x, + MOI.ScalarNonlinearFunction(:+, Any[f_add, f_add]) => + 0.0 * x * x + 2.0 + 2.0 * x, + MOI.ScalarNonlinearFunction(:^, Any[x, 2]) => 1.0 * x * x, + ) + @test convert(MOI.ScalarQuadraticFunction{Float64}, f) ≈ g + end + for f_error in (MOI.ScalarNonlinearFunction(:/, Any[1.0, x]),) + @test_throws( + InexactError, + convert(MOI.ScalarQuadraticFunction{Float64}, f_error), + ) + end + return +end + function runtests() for name in names(@__MODULE__; all = true) if startswith("$name", "test_")