Skip to content

Commit

Permalink
[Bridges] add ScalarQuadraticToScalarNonlinearBridge (#2233)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Jun 29, 2023
1 parent f9d1ee9 commit 5ac9cbb
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/submodules/Bridges/list_of_bridges.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/Bridges/Constraint/Constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
99 changes: 99 additions & 0 deletions src/Bridges/Constraint/bridges/functionize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
145 changes: 145 additions & 0 deletions src/functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions test/Bridges/Constraint/functionize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading

0 comments on commit 5ac9cbb

Please sign in to comment.