diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 1aeff32883..a05434a576 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -284,7 +284,7 @@ julia> MOI.Bridges.runtests( end, ) Test Summary: | Pass Total Time -Bridges.runtests | 32 32 0.8s +Bridges.runtests | 32 32 0.0s ``` """ function runtests(args...; kwargs...) @@ -294,12 +294,100 @@ function runtests(args...; kwargs...) return end +# This function tests that ConstraintDual is implemented correctly for a bridge +# by comparing Lagrangian values of the inner and outer models at corresponding +# primal points. Evaluating the functions also works for nonlinear expressions. +# +# For this test to be enabled, the bridge should implement `MOI.supports` and +# `MOI.set` for `ConstraintDual`. The easiest way to achieve this is with +# `Union{ConstraintDual,ConstraintDualStart}`. +function _test_dual( + Bridge::Type{<:AbstractBridge}, + input_fn::Function; + dual, + eltype, + model_eltype, +) + if Bridge <: Objective.AbstractBridge + return + end + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}()) + mock = MOI.Utilities.MockOptimizer( + inner; + eval_variable_constraint_dual = false, + ) + model = _bridged_model(Bridge{eltype}, mock) + input_fn(model) + final_touch(model) + final_touch(model) + MOI.set(mock, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + # Use an integer point so integer-coefficient vector functions can be + # evaluated without inexact conversions. Keep auxiliary primals at zero. + primal = 2 * one(model_eltype) + x = MOI.get(mock, MOI.ListOfVariableIndices()) + MOI.set(mock, MOI.VariablePrimal(), x, fill(zero(primal), length(x))) + if Bridge <: Variable.AbstractBridge + MOI.set(mock, MOI.VariablePrimal(), x, fill(primal, length(x))) + else + x = MOI.get(model, MOI.ListOfVariableIndices()) + MOI.set(model, MOI.VariablePrimal(), x, fill(primal, length(x))) + end + MOI.set(mock, MOI.DualStatus(), MOI.FEASIBLE_POINT) + # Some bridges leave auxiliary constraint duals unspecified. + for (F, S) in MOI.get(mock, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()) + set = MOI.get(mock, MOI.ConstraintSet(), ci) + MOI.set( + mock, + MOI.ConstraintDual(), + ci, + _fake_start(zero(model_eltype), set), + ) + end + end + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + if !MOI.supports(model, MOI.ConstraintDual(), MOI.ConstraintIndex{F,S}) + return + end + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + set = MOI.get(model, MOI.ConstraintSet(), ci) + value = _fake_start(dual, set) + MOI.set(model, MOI.ConstraintDual(), ci, value) + Test.@test MOI.get(model, MOI.ConstraintDual(), ci) ≈ value + end + end + Test.@test isapprox( + _test_lagrangian_value(model), + _test_lagrangian_value(mock); + atol = 1e-6, + ) + return +end + +function _test_lagrangian_value(model::MOI.ModelLike) + value = MOI.Utilities.get_fallback(model, MOI.ObjectiveValue()) + sign = MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE ? 1 : -1 + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + f = MOI.get(model, MOI.ConstraintFunction(), ci) + primal = MOI.Utilities.eval_variables(model, f) do vi + return MOI.get(model, MOI.VariablePrimal(), vi) + end + set = MOI.get(model, MOI.ConstraintSet(), ci) + dual = MOI.get(model, MOI.ConstraintDual(), ci) + value += sign * MOI.Utilities._dual_objective_dot(primal, dual, set) + end + end + return value +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, @@ -404,6 +492,11 @@ function _runtests( Test.@testset "Test delete" begin # COV_EXCL_LINE _test_delete(Bridge, model, inner) end + if !isnothing(dual) && !cannot_unbridge + Test.@testset "Test ConstraintDual" begin # COV_EXCL_LINE + _test_dual(Bridge, input_fn; dual, eltype, model_eltype) + end + end return end diff --git a/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl b/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl index e551bd13f6..7de3f09921 100644 --- a/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl +++ b/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl @@ -281,7 +281,7 @@ function conversion_cost( # This is most useful for conic solvers, which often support # VectorAffineFunction-in-S, but not VectorOfVariables-in-S. # - # The value in this function controls the cost of the transformation + # The value in this function controls the cost of the transformation # VectorOfVariables-in-S -> VectorAffineFunction-in-S. When we had the # previous cost of `1.0`, there might be an alternative bridge # VectorOfVariables-in-S -> VectorAffineFunction-in-S2 which also has a cost diff --git a/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl b/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl index 191868057a..87aae0a845 100644 --- a/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl +++ b/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl @@ -259,7 +259,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, ::Type{GeoMeanToPowerBridge{T,F}}, ) where {T,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.PowerCone{T}}) @@ -293,7 +293,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeanToPowerBridge{T,F}, start::AbstractVector{T}, ) where {T,F} @@ -327,7 +327,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeanToPowerBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl b/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl index d0894d6ec9..fb450921dd 100644 --- a/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl +++ b/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl @@ -198,7 +198,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{GeoMeantoRelEntrBridge{T,F,G,H}}, ) where {T,F,G,H} ci_1 = MOI.ConstraintIndex{F,MOI.Nonnegatives} @@ -277,28 +281,23 @@ end # Note log.(w/geomean(w)) = log.(w) .- sum(log, w) / n. function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeantoRelEntrBridge, value, ) d = length(value) - 1 u = value[1] w = value[2:end] - MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, [-u]) + MOI.set(model, attr, bridge.nn_index, [-u]) relentr_dual = vcat(-u / d, value[2:end], u / d * (log.(w) .+ (1 - sum(log, w) / d))) - MOI.set( - model, - MOI.ConstraintDualStart(), - bridge.relentr_index, - relentr_dual, - ) + MOI.set(model, attr, bridge.relentr_index, relentr_dual) return end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeantoRelEntrBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl b/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl index 97517549b2..85ea0783e8 100644 --- a/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl +++ b/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl @@ -101,7 +101,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{IntervalToHyperRectangleBridge{T,F,G}}, ) where {T,F,G} return MOI.supports( @@ -157,7 +161,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::IntervalToHyperRectangleBridge, value, ) diff --git a/src/Bridges/Constraint/bridges/NormNuclearBridge.jl b/src/Bridges/Constraint/bridges/NormNuclearBridge.jl index e57dfd53e1..68d04eef3e 100644 --- a/src/Bridges/Constraint/bridges/NormNuclearBridge.jl +++ b/src/Bridges/Constraint/bridges/NormNuclearBridge.jl @@ -137,7 +137,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::MOI.ConstraintPrimalStart, + attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDual}, ::Type{NormSpectralBridge{T,F,G}}, ) where {T,F,G} return MOI.supports( @@ -215,6 +215,28 @@ function MOI.get( return vcat(t, X) end +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintDual, + bridge::NormSpectralBridge{T}, + value::AbstractVector, +) where {T} + column_dim = bridge.column_dim + side_dim = bridge.row_dim + column_dim + dual = zeros(T, div(side_dim * (side_dim + 1), 2)) + # Invert the trace and the doubled off-diagonal entries in the getter. + for i in 1:side_dim + dual[MOI.Utilities.trimap(i, i)] = value[1] / side_dim + end + k = 2 + for j in 1:column_dim, i in (column_dim+1):side_dim + dual[MOI.Utilities.trimap(i, j)] = value[k] / 2 + k += 1 + end + MOI.set(model, attr, bridge.psd_index, dual) + return +end + """ NormNuclearBridge{T,F,G,H} <: Bridges.Constraint.AbstractBridge @@ -444,7 +466,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, ::Type{NormNuclearBridge{T,F,G,H}}, ) where {T,F,G,H} ci_1 = MOI.ConstraintIndex{F,MOI.GreaterThan{T}} @@ -494,11 +516,11 @@ end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormNuclearBridge{T}, value, ) where {T} - MOI.set(model, MOI.ConstraintDualStart(), bridge.ge_index, value[1]) + MOI.set(model, attr, bridge.ge_index, value[1]) column_dim = bridge.column_dim side_dim = bridge.row_dim + column_dim dual = zeros(T, div(side_dim * (side_dim + 1), 2)) @@ -510,17 +532,17 @@ function MOI.set( dual[MOI.Utilities.trimap(i, j)] = value[X_idx] / 2 X_idx += 1 end - MOI.set(model, MOI.ConstraintDualStart(), bridge.psd_index, dual) + MOI.set(model, attr, bridge.psd_index, dual) return end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormNuclearBridge{T}, ::Nothing, ) where {T} - MOI.set(model, MOI.ConstraintDualStart(), bridge.ge_index, nothing) - MOI.set(model, MOI.ConstraintDualStart(), bridge.psd_index, nothing) + MOI.set(model, attr, bridge.ge_index, nothing) + MOI.set(model, attr, bridge.psd_index, nothing) return end diff --git a/src/Bridges/Constraint/bridges/NormOneBridge.jl b/src/Bridges/Constraint/bridges/NormOneBridge.jl index e0221d5105..e257805b80 100644 --- a/src/Bridges/Constraint/bridges/NormOneBridge.jl +++ b/src/Bridges/Constraint/bridges/NormOneBridge.jl @@ -144,7 +144,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{NormOneBridge{T,F,G}}, ) where {T,F,G} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.Nonnegatives}) @@ -219,7 +223,7 @@ end # depending on the sense of `value[1 + i]`. function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormOneBridge, value, ) @@ -233,16 +237,16 @@ function MOI.set( nn_dual[1+d+i] = value[1+i] end end - MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, nn_dual) + MOI.set(model, attr, bridge.nn_index, nn_dual) return end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormOneBridge, ::Nothing, ) - MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, nothing) + MOI.set(model, attr, bridge.nn_index, nothing) return end diff --git a/src/Bridges/Constraint/bridges/NumberConversionBridge.jl b/src/Bridges/Constraint/bridges/NumberConversionBridge.jl index ca1f24fb47..0776fcfa1e 100644 --- a/src/Bridges/Constraint/bridges/NumberConversionBridge.jl +++ b/src/Bridges/Constraint/bridges/NumberConversionBridge.jl @@ -123,7 +123,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{NumberConversionBridge{T,F1,S1,F2,S2}}, ) where {T,F1,S1,F2,S2} return MOI.supports(model, attr, MOI.ConstraintIndex{F2,S2}) diff --git a/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl b/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl index 2f5519475f..7df2247cc8 100644 --- a/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl +++ b/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl @@ -185,7 +185,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{RelativeEntropyBridge{T,F,G,H}}, ) where {T,F,G,H} ci_1 = MOI.ConstraintIndex{F,MOI.GreaterThan{T}} @@ -275,7 +279,7 @@ end # r_i = exp(W(-w_i / (-v_i * e))) * (-v_i * e), but this is more complicated. function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::RelativeEntropyBridge, value, ) @@ -289,7 +293,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::RelativeEntropyBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl b/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl index 0d483a9797..1ce6ffaf6b 100644 --- a/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl +++ b/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl @@ -218,7 +218,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:RSOCtoPSDBridge{T,F,G}}, ) where {T,F,G} return MOI.supports( diff --git a/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl b/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl index da0a3fdb3e..c5445e4c15 100644 --- a/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl +++ b/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl @@ -80,7 +80,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:_AbstractSlackBridge{T,VF,ZS,F,S}}, ) where {T,VF,ZS,F,S} ret = true @@ -152,7 +156,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::_AbstractSlackBridge{T}, value, ) where {T} diff --git a/src/Bridges/Constraint/bridges/ScalarizeBridge.jl b/src/Bridges/Constraint/bridges/ScalarizeBridge.jl index 0038cb4553..ea4074a35d 100644 --- a/src/Bridges/Constraint/bridges/ScalarizeBridge.jl +++ b/src/Bridges/Constraint/bridges/ScalarizeBridge.jl @@ -153,7 +153,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{ScalarizeBridge{T,F,S}}, ) where {T,F,S} return MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) @@ -211,7 +215,7 @@ end function MOI.get( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::ScalarizeBridge, ) values = MOI.get.(model, attr, bridge.scalar_constraints) @@ -223,7 +227,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::ScalarizeBridge, value, ) diff --git a/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl b/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl index ebebe8e8cb..e330031adf 100644 --- a/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl @@ -160,7 +160,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SplitComplexEqualToBridge{T,F}}, ) where {T,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.EqualTo{T}}) @@ -194,7 +198,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexEqualToBridge{T}, value, ) where {T} @@ -209,7 +217,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexEqualToBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl b/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl index 3e12f77136..0a626c2c81 100644 --- a/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl @@ -158,7 +158,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SplitComplexZerosBridge{T,F}}, ) where {T,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.Zeros}) @@ -190,7 +194,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexZerosBridge{T}, value, ) where {T} @@ -210,7 +218,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexZerosBridge{T}, ::Nothing, ) where {T} diff --git a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl index 5309b56f08..ebfb728d69 100644 --- a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl @@ -196,7 +196,11 @@ 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}) @@ -280,7 +284,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitHyperRectangleBridge{T}, ::Nothing, ) where {T} @@ -293,7 +297,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitHyperRectangleBridge{T}, value::AbstractVector{T}, ) where {T} diff --git a/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl b/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl index af393a1863..41e01b7ab0 100644 --- a/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl @@ -211,7 +211,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{SplitIntervalBridge{T,F,S,LS,US}}, ) where {T,F,S,LS,US} ci_1 = MOI.ConstraintIndex{F,LS} @@ -296,7 +300,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitIntervalBridge{T}, value, ) where {T} diff --git a/src/Bridges/Constraint/bridges/SquareBridge.jl b/src/Bridges/Constraint/bridges/SquareBridge.jl index fe3132a791..1f7302444e 100644 --- a/src/Bridges/Constraint/bridges/SquareBridge.jl +++ b/src/Bridges/Constraint/bridges/SquareBridge.jl @@ -279,7 +279,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{SquareBridge{T,F,G,TT,ST}}, ) where {T,F,G,TT,ST} return MOI.supports(model, attr, MOI.ConstraintIndex{F,TT}) & @@ -417,7 +421,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SquareBridge, value, ) @@ -453,7 +457,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SquareBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/VectorizeBridge.jl b/src/Bridges/Constraint/bridges/VectorizeBridge.jl index 50a1ea4854..26bda2be6e 100644 --- a/src/Bridges/Constraint/bridges/VectorizeBridge.jl +++ b/src/Bridges/Constraint/bridges/VectorizeBridge.jl @@ -108,7 +108,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{VectorizeBridge{T,F,S,G}}, ) where {T,F,S,G} return MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) @@ -183,7 +187,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::VectorizeBridge, value, ) diff --git a/src/Bridges/Constraint/set_map.jl b/src/Bridges/Constraint/set_map.jl index 70062db687..450c99f3d2 100644 --- a/src/Bridges/Constraint/set_map.jl +++ b/src/Bridges/Constraint/set_map.jl @@ -24,7 +24,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart,MOI.ConstraintDual}, ::Type{<:BridgeType{T,F2,F3}}, ) where {T,F2,F3} return MOI.supports(model, attr, MOI.ConstraintIndex{F2,S2}) || @@ -205,7 +205,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::BT, value, ) where {BT<:MultiSetMapBridge} @@ -263,7 +263,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SetMapBridge{T,S2,S1,F}}, ) where {T,S2,S1,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,S2}) diff --git a/src/Bridges/Variable/set_map.jl b/src/Bridges/Variable/set_map.jl index 21c9af8105..2b3a53ad8e 100644 --- a/src/Bridges/Variable/set_map.jl +++ b/src/Bridges/Variable/set_map.jl @@ -155,7 +155,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SetMapBridge, value, ) @@ -272,7 +272,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SetMapBridge{T,S1}}, ) where {T,S1<:MOI.AbstractScalarSet} return MOI.supports(model, attr, MOI.ConstraintIndex{MOI.VariableIndex,S1}) @@ -351,7 +355,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SetMapBridge{T,S1}}, ) where {T,S1<:MOI.AbstractVectorSet} CI = MOI.ConstraintIndex{MOI.VectorOfVariables,S1} diff --git a/src/Utilities/results.jl b/src/Utilities/results.jl index d1f199334d..b7ba290d6e 100644 --- a/src/Utilities/results.jl +++ b/src/Utilities/results.jl @@ -122,6 +122,8 @@ _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.Parameter}) = true + _variable_set_in_dual_objective(::Type{<:MOI.GreaterThan}) = true _variable_set_in_dual_objective(::Type{<:MOI.LessThan}) = true @@ -130,9 +132,11 @@ _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) = real(set_dot(x, y, set)) -_dual_objective_dot(x, y, set::MOI.EqualTo) = (x - set.value) * y +function _dual_objective_dot(x, y, set::Union{MOI.EqualTo,MOI.Parameter}) + return real(set_dot(x - set.value, y, set)) +end _dual_objective_dot(x, y, set::MOI.LessThan) = (x - set.upper) * y diff --git a/src/Utilities/set_dot.jl b/src/Utilities/set_dot.jl index 5abbdc7641..32d04e3156 100644 --- a/src/Utilities/set_dot.jl +++ b/src/Utilities/set_dot.jl @@ -39,9 +39,9 @@ function triangle_dot( for j in 1:i k += 1 if i == j - result = MA.add_mul!!(result, x[k], y[k]) + result = MA.add_mul!!(result, conj(x[k]), y[k]) else - result = MA.add_mul!!(result, 2, x[k], y[k]) + result = MA.add_mul!!(result, 2, conj(x[k]), y[k]) end end end diff --git a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl index eec295bef8..430f59590c 100644 --- a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl +++ b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl @@ -246,17 +246,19 @@ function test_runtests() 1.0 * x <= 1.0 """, ) - MOI.Bridges.runtests( - MOI.Bridges.Constraint.ScalarFunctionizeBridge, - """ - variables: x - x in ZeroOne() - """, - """ - variables: x - 1.0 * x in ZeroOne() - """, - ) + for set in ("ZeroOne()", "Integer()") + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ScalarFunctionizeBridge, + """ + variables: x + x in $set + """, + """ + variables: x + 1.0 * x in $set + """, + ) + end MOI.Bridges.runtests( MOI.Bridges.Constraint.VectorFunctionizeBridge, """ @@ -290,6 +292,19 @@ function test_runtests() [1.0 * t, 1.0 * x] in SecondOrderCone(2) """, ) + for set in ("SOS1([1.0, 2.0])", "SOS2([1.0, 2.0])") + MOI.Bridges.runtests( + MOI.Bridges.Constraint.VectorFunctionizeBridge, + """ + variables: x, y + [x, y] in $set + """, + """ + variables: x, y + [1.0 * x, 1.0 * y] in $set + """, + ) + end return end @@ -333,6 +348,27 @@ function test_FunctionConversionBridge() return end +function test_FunctionConversionBridge_ConstraintDual() + inner = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + model = MOI.Bridges.Constraint.ToScalarNonlinear{Float64}(inner) + x, y = MOI.add_variables(model, 2) + f = 1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0 + ci = MOI.add_constraint(model, f, MOI.GreaterThan(1.0)) + bridge = MOI.get(model, MOI.Bridges.FirstBridge(), ci) + @test MOI.get(model, MOI.ConstraintFunction(), ci) ≈ f + @test MOI.get(inner, MOI.ConstraintFunction(), bridge.constraint) ≈ + convert(MOI.ScalarNonlinearFunction, f) + @test MOI.supports(model, MOI.ConstraintDual(), typeof(ci)) + for dual in (-1.2, 0.0, 1.2) + MOI.set(model, MOI.ConstraintDual(), ci, dual) + @test MOI.get(model, MOI.ConstraintDual(), ci) == dual + @test MOI.get(inner, MOI.ConstraintDual(), bridge.constraint) == dual + end + return +end + function test_FunctionConversionBridge_VectorNonlinearFunction() inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) BT = MOI.Bridges.Constraint.FunctionConversionBridge{ diff --git a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl index 33c39ccc98..f7f7a84e9e 100644 --- a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl +++ b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl @@ -95,6 +95,7 @@ function test_runtests_free_row() [1.0 * z + -3.0, 3.3 + -1.0 * z] in Nonnegatives(2) """; constraint_start = 3.0, + dual = [0.0, 3.0], ) return end @@ -108,7 +109,8 @@ function test_runtests_all_free_rows() """, """ variables: x - """, + """; + dual = [0.0], ) return end diff --git a/test/Bridges/Variable/test_VectorizeBridge.jl b/test/Bridges/Variable/test_VectorizeBridge.jl index 33936de912..3d44061b34 100644 --- a/test/Bridges/Variable/test_VectorizeBridge.jl +++ b/test/Bridges/Variable/test_VectorizeBridge.jl @@ -287,6 +287,39 @@ function test_runtests() return end +function test_ConstraintDual_lagrangian_shift() + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.Model{Float64}(); + eval_variable_constraint_dual = false, + ) + model = MOI.Bridges.Variable.Vectorize{Float64}(mock) + x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(2.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + 1.0 * x, + ) + y = only(MOI.get(mock, MOI.ListOfVariableIndices())) + ci = only( + MOI.get( + mock, + MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, + MOI.Nonnegatives, + }(), + ), + ) + MOI.set(mock, MOI.VariablePrimal(), y, 3.0) + MOI.set(mock, MOI.ConstraintDual(), ci, [1.2]) + MOI.set(mock, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + # The dual is deliberately nonstationary: the coefficient of y in the + # Lagrangian is 1 - 1.2. Both models should nevertheless give 5 - 3 * 1.2. + @test MOI.Bridges._test_lagrangian_value(model) ≈ 1.4 + @test MOI.Bridges._test_lagrangian_value(mock) ≈ 1.4 + return +end + function test_list_of_constraint_indices() inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) model = MOI.Bridges.Variable.Vectorize{Float64}(inner) diff --git a/test/Utilities/test_mockoptimizer.jl b/test/Utilities/test_mockoptimizer.jl index 1d297f72b6..d78f5f0045 100644 --- a/test/Utilities/test_mockoptimizer.jl +++ b/test/Utilities/test_mockoptimizer.jl @@ -191,6 +191,26 @@ function test_DualObjectiveValue() return end +function test_ConstraintDual_discrete_sets() + mock = MOIU.MockOptimizer( + MOIU.UniversalFallback(MOIU.Model{Float64}()); + eval_variable_constraint_dual = false, + ) + x, y = MOI.add_variables(mock, 2) + for (f, set, dual) in ( + (x, MOI.ZeroOne(), 1.2), + (y, MOI.Integer(), -2.3), + (MOI.VectorOfVariables([x, y]), MOI.SOS1([1.0, 2.0]), [1.2, -2.3]), + (MOI.VectorOfVariables([x, y]), MOI.SOS2([1.0, 2.0]), [-2.3, 1.2]), + ) + ci = MOI.add_constraint(mock, f, set) + @test MOI.supports(mock, MOI.ConstraintDual(), typeof(ci)) + MOI.set(mock, MOI.ConstraintDual(), ci, dual) + @test MOI.get(mock, MOI.ConstraintDual(), ci) == dual + end + return +end + function test_add_con_allowed_false() model = MOI.Utilities.MockOptimizer( MOI.Utilities.Model{Float64}(); diff --git a/test/Utilities/test_results.jl b/test/Utilities/test_results.jl index 6b0c685dda..7e82bc5173 100644 --- a/test/Utilities/test_results.jl +++ b/test/Utilities/test_results.jl @@ -291,6 +291,62 @@ function test_get_fallback_DualObjectiveValue() return end +function test_get_fallback_DualObjectiveValue_complex() + model = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + x = MOI.add_variable(model) + c1 = MOI.add_constraint( + model, + (1.0 + 2.0im) * x + (3.0 + 4.0im), + MOI.EqualTo(1.0 - 2.0im), + ) + c2 = MOI.add_constraint( + model, + MOI.Utilities.vectorize([ + (1.0 + 2.0im) * x + (3.0 + 4.0im), + -1.0im * x + (-2.0 + 5.0im), + ]), + MOI.Zeros(2), + ) + MOI.set(model, MOI.ConstraintDual(), c1, 2.0 - 3.0im) + MOI.set(model, MOI.ConstraintDual(), c2, [2.0 - 3.0im, -1.0 + 4.0im]) + MOI.set(model, MOI.DualStatus(), MOI.FEASIBLE_POINT) + for (sense, expected) in ((MOI.MIN_SENSE, -2.0), (MOI.MAX_SENSE, 2.0)) + MOI.set(model, MOI.ObjectiveSense(), sense) + @test MOI.Utilities.get_fallback( + model, + MOI.DualObjectiveValue(), + Float64, + ) == expected + end + return +end + +function test_get_fallback_DualObjectiveValue_Parameter() + model = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()); + eval_variable_constraint_dual = false, + ) + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + 1.0 * x + 5.0, + ) + MOI.set(model, MOI.ConstraintDual(), ci, 3.0) + MOI.set(model, MOI.DualStatus(), MOI.FEASIBLE_POINT) + for (sense, expected) in ((MOI.MIN_SENSE, 11.0), (MOI.MAX_SENSE, -1.0)) + MOI.set(model, MOI.ObjectiveSense(), sense) + @test MOI.Utilities.get_fallback( + model, + MOI.DualObjectiveValue(), + Float64, + ) == expected + end + return +end + function test_get_fallback_DualObjectiveValue_Quadratic() T = Float64 model = MOI.Utilities.MockOptimizer( diff --git a/test/Utilities/test_set_dot.jl b/test/Utilities/test_set_dot.jl index 969e0442c4..b9abe45406 100644 --- a/test/Utilities/test_set_dot.jl +++ b/test/Utilities/test_set_dot.jl @@ -53,6 +53,15 @@ function test_set_dot(T = Int) end end +function test_set_dot_complex_triangle() + x = ComplexF64[1+2im, 3-4im, -2+5im] + y = ComplexF64[2-3im, -1+2im, 4+1im] + set = MOI.PositiveSemidefiniteConeTriangle(2) + @test MOI.Utilities.set_dot(x, y, set) == -29 - 25im + @test MOI.Utilities.set_dot(x, x, set) == 84 + return +end + end TestSetDot.runtests()