Skip to content
Open
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
4 changes: 2 additions & 2 deletions docs/src/submodules/Bridges/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ julia> MOI.Bridges.runtests(
""",
)
Test Summary: | Pass Total Time
Bridges.runtests | 29 29 0.0s
Bridges.runtests | 30 30 0.0s
```

There are a number of other useful keyword arguments.
Expand Down Expand Up @@ -123,5 +123,5 @@ Subject to:
ScalarAffineFunction{Int64}-in-LessThan{Int64}
(0) - (1) x <= (-1)
Test Summary: | Pass Total Time
Bridges.runtests | 29 29 0.0s
Bridges.runtests | 30 30 0.0s
```
55 changes: 54 additions & 1 deletion src/Bridges/Bridges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ julia> MOI.Bridges.runtests(
end,
)
Test Summary: | Pass Total Time
Bridges.runtests | 32 32 0.8s
Bridges.runtests | 33 33 0.8s
```
"""
function runtests(args...; kwargs...)
Expand All @@ -293,12 +293,60 @@ function runtests(args...; kwargs...)
return
end

# A good way to check that the linear mapping implemented in the setter of
# `ConstraintDual` is the inverse-adjoint of the mapping implemented in the
# constraint transformation is to check `get_fallback` for `DualObjectiveValue`.
# Indeed, it will check that the inner product between the constraint constants
# and the dual is the same before and after the bridge transformations.
# For this test to be enabled, the bridge should implement `supports`
# for `ConstraintDual` and implement `MOI.set` for `ConstraintDual`.
# Typically, this would be achieved using
# `Union{ConstraintDual,ConstraintDualStart}` for `MOI.get`, `MOI.set` and
# `MOI.supports`
function _test_dual(
Bridge::Type{<:AbstractBridge},
input_fn::Function;
dual,
eltype,
model_eltype,
)
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}())
mock = MOI.Utilities.MockOptimizer(inner)
model = _bridged_model(Bridge{eltype}, mock)
input_fn(model)
final_touch(model)
# Should be able to call final_touch multiple times.
final_touch(model)
# If the bridges does not support `ConstraintDualStart`, it probably won't
# support `ConstraintDual` so we skip these tests
list_of_constraints = MOI.get(model, MOI.ListOfConstraintTypesPresent())
attr = MOI.ConstraintDual()
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean

Suggested change
attr = MOI.ConstraintDual()
attr = MOI.ConstraintDualStart()

Copy link
Member Author

Choose a reason for hiding this comment

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

No, if the bridge don't support ConstraintDual, we skip the test. Because get_fallback don't work with ConstraintDualStart, we need to use ConstraintDual

for (F, S) in list_of_constraints
if !MOI.supports(model, attr, MOI.ConstraintIndex{F,S})
# We need all duals for `DualObjectiveValue` fallback
# TODO except the ones with no constants, we could ignore them
return
end
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
set = MOI.get(model, MOI.ConstraintSet(), ci)
MOI.set(model, MOI.ConstraintDual(), ci, _fake_start(dual, set))
end
end
model_dual =
MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), eltype)
mock_dual =
MOI.Utilities.get_fallback(mock, MOI.DualObjectiveValue(), eltype)
# Need `atol` in case one of them is zero and the other one almost zero
Test.@test model_dual ≈ mock_dual atol = 1e-6
end

function _runtests(
Bridge::Type{<:AbstractBridge},
input_fn::Function,
output_fn::Function;
variable_start = 1.2,
constraint_start = 1.2,
dual = constraint_start,
eltype = Float64,
model_eltype = eltype,
print_inner_model::Bool = false,
Expand Down Expand Up @@ -403,6 +451,11 @@ function _runtests(
Test.@testset "Test delete" begin # COV_EXCL_LINE
_test_delete(Bridge, model, inner)
end
if !isnothing(dual)
Test.@testset "Test ConstraintDual" begin
_test_dual(Bridge, input_fn; dual, eltype, model_eltype)
end
end
return
end

Expand Down
21 changes: 17 additions & 4 deletions src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,28 @@ end

function MOI.supports(
model::MOI.ModelLike,
attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart},
attr::Union{
MOI.ConstraintPrimalStart,
MOI.ConstraintDualStart,
MOI.ConstraintDual,
},
::Type{<:SplitHyperRectangleBridge{T,G}},
) where {T,G}
return MOI.supports(model, attr, MOI.ConstraintIndex{G,MOI.Nonnegatives})
end

_get_free_start(bridge, ::MOI.ConstraintDualStart) = bridge.free_dual_start
function _get_free_start(
bridge,
::Union{MOI.ConstraintDualStart,MOI.ConstraintDual},
)
return bridge.free_dual_start
end

function _set_free_start(bridge, ::MOI.ConstraintDualStart, value)
function _set_free_start(
bridge,
::Union{MOI.ConstraintDualStart,MOI.ConstraintDual},
value,
)
bridge.free_dual_start = value
return
end
Expand Down Expand Up @@ -284,7 +297,7 @@ end

function MOI.set(
model::MOI.ModelLike,
attr::MOI.ConstraintDualStart,
attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual},
bridge::SplitHyperRectangleBridge{T},
values::AbstractVector{T},
) where {T}
Expand Down
172 changes: 66 additions & 106 deletions src/Utilities/results.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,112 +52,6 @@ end

# MOI.DualObjectiveValue

function _constraint_constant(
model::MOI.ModelLike,
ci::MOI.ConstraintIndex{
<:MOI.AbstractVectorFunction,
<:MOI.AbstractVectorSet,
},
::Type{T},
) where {T}
return MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T)
end

function _constraint_constant(
model::MOI.ModelLike,
ci::MOI.ConstraintIndex{
<:MOI.AbstractScalarFunction,
<:MOI.AbstractScalarSet,
},
::Type{T},
) where {T}
return MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T) -
MOI.constant(MOI.get(model, MOI.ConstraintSet(), ci))
end

function _dual_objective_value(
model::MOI.ModelLike,
ci::MOI.ConstraintIndex,
::Type{T},
result_index::Integer,
) where {T}
return set_dot(
_constraint_constant(model, ci, T),
MOI.get(model, MOI.ConstraintDual(result_index), ci),
MOI.get(model, MOI.ConstraintSet(), ci),
)
end

"""
Given lower <= f(x) <= upper [dual], return the expression to be multiplied by
the dual variable. This is one of the following cases:

1. f(x) - lower: if `lower > -Inf` and the lower bound is binding (either no
`upper` or `dual > 0`)
2. f(x) - upper: if `upper < Inf` and the upper bound is binding (either no
`lower` or `dual < 0`)
3. f(x): if `lower = -Inf` and `upper = Inf` or `dual = 0`
"""
function _constant_minus_bound(constant, lower, upper, dual)
if isfinite(lower) && (!isfinite(upper) || dual > zero(dual))
return constant - lower
elseif isfinite(upper) && (!isfinite(lower) || dual < zero(dual))
return constant - upper
else
return constant
end
end

function _dual_objective_value(
model::MOI.ModelLike,
ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Interval},
::Type{T},
result_index::Integer,
) where {T}
constant = MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T)
set = MOI.get(model, MOI.ConstraintSet(), ci)
dual = MOI.get(model, MOI.ConstraintDual(result_index), ci)
constant = _constant_minus_bound(constant, set.lower, set.upper, dual)
return set_dot(constant, dual, set)
end

function _dual_objective_value(
model::MOI.ModelLike,
ci::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction,<:MOI.HyperRectangle},
::Type{T},
result_index::Integer,
) where {T}
func_constant =
MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T)
set = MOI.get(model, MOI.ConstraintSet(), ci)
dual = MOI.get(model, MOI.ConstraintDual(result_index), ci)
constants = map(enumerate(func_constant)) do (i, c)
return _constant_minus_bound(c, set.lower[i], set.upper[i], dual[i])
end
return set_dot(constants, dual, set)
end

function _dual_objective_value(
model::MOI.ModelLike,
::Type{F},
::Type{S},
::Type{T},
result_index::Integer,
) where {T,F<:MOI.AbstractFunction,S<:MOI.AbstractSet}
value = zero(T)
if F == variable_function_type(S) && !_has_constant(S)
return value # Shortcut
end
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
value += _dual_objective_value(model, ci, T, result_index)
end
return value
end

_has_constant(::Type{<:MOI.AbstractScalarSet}) = true
_has_constant(::Type{<:MOI.AbstractVectorSet}) = false
_has_constant(::Type{<:MOI.HyperRectangle}) = true

"""
get_fallback(
model::MOI.ModelLike,
Expand Down Expand Up @@ -192,6 +86,72 @@ function get_fallback(
return value::T
end

function _dual_objective_value(
model::MOI.ModelLike,
::Type{F},
::Type{S},
::Type{T},
result_index::Integer,
)::T where {T,F<:MOI.AbstractFunction,S<:MOI.AbstractSet}
value = zero(T)
if F == variable_function_type(S) && !_variable_set_in_dual_objective(S)
# Early return. This is a constraint like x in R_+, so no contribution
# appears in the dual objective.
return value
end
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
constant = MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T)
set = MOI.get(model, MOI.ConstraintSet(), ci)
dual = MOI.get(model, MOI.ConstraintDual(result_index), ci)
value += _dual_objective_dot(constant, dual, set)
end
return value
end

_variable_set_in_dual_objective(::Type{<:MOI.AbstractSet}) = false

_variable_set_in_dual_objective(::Type{<:MOI.EqualTo}) = true

_variable_set_in_dual_objective(::Type{<:MOI.GreaterThan}) = true

_variable_set_in_dual_objective(::Type{<:MOI.LessThan}) = true

_variable_set_in_dual_objective(::Type{<:MOI.Interval}) = true

_variable_set_in_dual_objective(::Type{<:MOI.HyperRectangle}) = true

_dual_objective_dot(x, y, set) = set_dot(x, y, set)

_dual_objective_dot(x, y, set::MOI.EqualTo) = (x - set.value) * y

_dual_objective_dot(x, y, set::MOI.LessThan) = (x - set.upper) * y

_dual_objective_dot(x, y, set::MOI.GreaterThan) = (x - set.lower) * y

function _dual_objective_dot(x, y, set::MOI.Interval)
if isfinite(set.lower) && (!isfinite(set.upper) || y > zero(y))
return (x - set.lower) * y
elseif isfinite(set.upper) && (!isfinite(set.lower) || y < zero(y))
return (x - set.upper) * y
end
return x * y
end

function _dual_objective_dot(x, y, set::MOI.HyperRectangle)
@assert length(x) == length(y) == MOI.dimension(set)
ret = zero(eltype(x))
for (xi, yi, li, ui) in zip(x, y, set.lower, set.upper)
if isfinite(li) && (!isfinite(ui) || yi > zero(yi))
ret += (xi - li) * yi
elseif isfinite(ui) && (!isfinite(li) || yi < zero(yi))
ret += (xi - ui) * yi
else
ret += xi * yi
end
end
return ret
end

# MOI.ConstraintPrimal

"""
Expand Down
2 changes: 1 addition & 1 deletion test/Bridges/Constraint/NormSpectralBridge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function test_NormNuclear()
mock,
var_primal,
(MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
[[1.0]],
[1.0],
(
MOI.VectorAffineFunction{Float64},
MOI.PositiveSemidefiniteConeTriangle,
Expand Down
1 change: 1 addition & 0 deletions test/Bridges/Constraint/ScalarFunctionizeBridge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ function test_FunctionConversionBridge()
variables: x, y
ScalarNonlinearFunction(1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0) >= 1.0
""",
dual = nothing, # `get_fallback` ignores the constant `4.0` of the function
)
# VectorAffineFunction -> VectorQuadraticFunction
MOI.Bridges.runtests(
Expand Down
Loading