Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bridges] add ScalarQuadraticToScalarNonlinearBridge #2233

Merged
merged 3 commits into from
Jun 29, 2023
Merged
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
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method and the next two should be implemented at the AbstractFunctionConversion level

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this can be done in a separate PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah there's the canonical issue as well. But we can come back to it with #2224

::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