From 85a1db583eb80d3eb90bbb1c5bfa27f714584ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 23 Jun 2024 00:43:43 +0200 Subject: [PATCH] Allow SetMapBridge to use bridge value (#2509) --- docs/src/submodules/Bridges/reference.md | 1 + src/Bridges/Constraint/set_map.jl | 49 +++++- src/Bridges/set_map.jl | 113 ++++++++++---- test/Bridges/set_map.jl | 185 +++++++++++++++++++++++ 4 files changed, 313 insertions(+), 35 deletions(-) create mode 100644 test/Bridges/set_map.jl diff --git a/docs/src/submodules/Bridges/reference.md b/docs/src/submodules/Bridges/reference.md index 753c60919f..25047bf95f 100644 --- a/docs/src/submodules/Bridges/reference.md +++ b/docs/src/submodules/Bridges/reference.md @@ -90,6 +90,7 @@ Bridges.debug_supports ## [SetMap API](@id constraint_set_map) ```@docs +Bridges.MapNotInvertible Bridges.map_set Bridges.inverse_map_set Bridges.map_function diff --git a/src/Bridges/Constraint/set_map.jl b/src/Bridges/Constraint/set_map.jl index 0232d8cdd5..d132b78a0f 100644 --- a/src/Bridges/Constraint/set_map.jl +++ b/src/Bridges/Constraint/set_map.jl @@ -96,13 +96,29 @@ end # Attributes, Bridge acting as a constraint +# MapNotInvertible is thrown if the bridge does not support inverting the +# function. The user doesn't need to know this, only that they cannot get the +# attribute. Throwing `GetAttributeNotAllowed` allows `CachingOptimizer` to fall +# back to using the cache. +function _not_invertible_error_message(attr, message) + return "Cannot get `$attr` as the constraint is reformulated through a linear transformation that is not invertible. $message" +end + function MOI.get( model::MOI.ModelLike, attr::MOI.ConstraintFunction, bridge::MultiSetMapBridge{T,S1,G}, ) where {T,S1,G} mapped_func = MOI.get(model, attr, bridge.constraint) - func = MOI.Bridges.inverse_map_function(typeof(bridge), mapped_func) + func = try + MOI.Bridges.inverse_map_function(bridge, mapped_func) + catch err + if err isa MOI.Bridges.MapNotInvertible + msg = _not_invertible_error_message(attr, err.message) + throw(MOI.GetAttributeNotAllowed(attr, msg)) + end + rethrow(err) + end return MOI.Utilities.convert_approx(G, func) end @@ -123,7 +139,7 @@ function MOI.get( bridge::MultiSetMapBridge, ) set = MOI.get(model, attr, bridge.constraint) - return MOI.Bridges.inverse_map_set(typeof(bridge), set) + return MOI.Bridges.inverse_map_set(bridge, set) end function MOI.set( @@ -132,7 +148,7 @@ function MOI.set( bridge::MultiSetMapBridge{T,S1}, set::S1, ) where {T,S1} - new_set = MOI.Bridges.map_set(typeof(bridge), set) + new_set = MOI.Bridges.map_set(bridge, set) MOI.set(model, attr, bridge.constraint, new_set) return end @@ -146,7 +162,18 @@ function MOI.get( if value === nothing return nothing end - return MOI.Bridges.inverse_map_function(typeof(bridge), value) + try + return MOI.Bridges.inverse_map_function(bridge, value) + catch err + # MapNotInvertible is thrown if the bridge does not support inverting + # the function. The user doesn't need to know this, only that they + # cannot get the attribute. + if err isa MOI.Bridges.MapNotInvertible + msg = _not_invertible_error_message(attr, err.message) + throw(MOI.GetAttributeNotAllowed(attr, msg)) + end + rethrow(err) + end end function MOI.set( @@ -158,7 +185,7 @@ function MOI.set( if value === nothing MOI.set(model, attr, bridge.constraint, nothing) else - mapped_value = MOI.Bridges.map_function(typeof(bridge), value) + mapped_value = MOI.Bridges.map_function(bridge, value) MOI.set(model, attr, bridge.constraint, mapped_value) end return @@ -173,7 +200,7 @@ function MOI.get( if value === nothing return nothing end - return MOI.Bridges.adjoint_map_function(typeof(bridge), value) + return MOI.Bridges.adjoint_map_function(bridge, value) end function MOI.set( @@ -185,7 +212,15 @@ function MOI.set( if value === nothing MOI.set(model, attr, bridge.constraint, nothing) else - mapped_value = MOI.Bridges.inverse_adjoint_map_function(BT, value) + mapped_value = try + MOI.Bridges.inverse_adjoint_map_function(bridge, value) + catch err + if err isa MOI.Bridges.MapNotInvertible + msg = _not_invertible_error_message(attr, err.message) + throw(MOI.SetAttributeNotAllowed(attr, msg)) + end + rethrow(err) + end MOI.set(model, attr, bridge.constraint, mapped_value) end return diff --git a/src/Bridges/set_map.jl b/src/Bridges/set_map.jl index d43796105b..c665c1dd00 100644 --- a/src/Bridges/set_map.jl +++ b/src/Bridges/set_map.jl @@ -5,35 +5,69 @@ # in the LICENSE.md file or at https://opensource.org/licenses/MIT. """ + struct MapNotInvertible <: Exception + message::String + end + +An error thrown by [`inverse_map_function`](@ref) or +[`inverse_adjoint_map_function`](@ref) indicating that the linear map `A` +defined in [`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref) +is not invertible. +""" +struct MapNotInvertible <: Exception + message::String +end + +""" + map_set(bridge::MOI.Bridges.AbstractBridge, set) map_set(::Type{BT}, set) where {BT} Return the image of `set` through the linear map `A` defined in -[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). This is -used for bridging the constraint and setting -the [`MOI.ConstraintSet`](@ref). +[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). + +This function is used for bridging the constraint and setting the +[`MOI.ConstraintSet`](@ref). """ -function map_set end +map_set(bridge::AbstractBridge, set) = map_set(typeof(bridge), set) """ + inverse_map_set(bridge::MOI.Bridges.AbstractBridge, set) inverse_map_set(::Type{BT}, set) where {BT} Return the preimage of `set` through the linear map `A` defined in -[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). This is -used for getting the [`MOI.ConstraintSet`](@ref). +[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). + +This function is used for getting the [`MOI.ConstraintSet`](@ref). + +The method can alternatively be defined on the bridge type. This legacy +interface is kept for backward compatibility. """ -function inverse_map_set end +function inverse_map_set(bridge::AbstractBridge, set) + return inverse_map_set(typeof(bridge), set) +end """ + map_function(bridge::MOI.Bridges.AbstractBridge, func) map_function(::Type{BT}, func) where {BT} Return the image of `func` through the linear map `A` defined in -[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). This is -used for getting the [`MOI.ConstraintPrimal`](@ref) of variable +[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). + +This function is used for getting the [`MOI.ConstraintPrimal`](@ref) of variable bridges. For constraint bridges, this is used for bridging the constraint, -setting the [`MOI.ConstraintFunction`](@ref) and -[`MOI.ConstraintPrimalStart`](@ref) and -modifying the function with [`MOI.modify`](@ref). +setting the [`MOI.ConstraintFunction`](@ref) and [`MOI.ConstraintPrimalStart`](@ref) +and modifying the function with [`MOI.modify`](@ref). +The default implementation of [`Constraint.bridge_constraint`](@ref) uses +[`map_function`](@ref) with the bridge type so if this function is defined +on the bridge type, [`Constraint.bridge_constraint`](@ref) does not need +to be implemented. +""" +function map_function(bridge::AbstractBridge, func) + return map_function(typeof(bridge), func) +end + +""" map_function(::Type{BT}, func, i::IndexInVector) where {BT} Return the scalar function at the `i`th index of the vector function that @@ -42,42 +76,65 @@ would be returned by `map_function(BT, func)` except that it may compute the the [`MOI.VariablePrimal`](@ref) and [`MOI.VariablePrimalStart`](@ref) of variable bridges. """ -function map_function end - function map_function(::Type{BT}, func, i::IndexInVector) where {BT} return MOI.Utilities.eachscalar(map_function(BT, func))[i.value] end """ + inverse_map_function(bridge::MOI.Bridges.AbstractBridge, func) inverse_map_function(::Type{BT}, func) where {BT} Return the image of `func` through the inverse of the linear map `A` defined in -[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). This is -used by [`Variable.unbridged_map`](@ref) and for setting the -[`MOI.VariablePrimalStart`](@ref) of variable bridges -and for getting the [`MOI.ConstraintFunction`](@ref), -the [`MOI.ConstraintPrimal`](@ref) and the +[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). + +This function is used by [`Variable.unbridged_map`](@ref) and for setting the +[`MOI.VariablePrimalStart`](@ref) of variable bridges and for getting the +[`MOI.ConstraintFunction`](@ref), the [`MOI.ConstraintPrimal`](@ref) and the [`MOI.ConstraintPrimalStart`](@ref) of constraint bridges. + +If the linear map `A` is not invertible, the error [`MapNotInvertible`](@ref) is +thrown. + +The method can alternatively be defined on the bridge type. This legacy +interface is kept for backward compatibility. """ -function inverse_map_function end +function inverse_map_function(bridge::AbstractBridge, func) + return inverse_map_function(typeof(bridge), func) +end """ + adjoint_map_function(bridge::MOI.Bridges.AbstractBridge, func) adjoint_map_function(::Type{BT}, func) where {BT} Return the image of `func` through the adjoint of the linear map `A` defined in -[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). This is -used for getting the [`MOI.ConstraintDual`](@ref) and +[`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). + +This function is used for getting the [`MOI.ConstraintDual`](@ref) and [`MOI.ConstraintDualStart`](@ref) of constraint bridges. + +The method can alternatively be defined on the bridge type. This legacy +interface is kept for backward compatibility. """ -function adjoint_map_function end +function adjoint_map_function(bridge::AbstractBridge, func) + return adjoint_map_function(typeof(bridge), func) +end """ + inverse_adjoint_map_function(bridge::MOI.Bridges.AbstractBridge, func) inverse_adjoint_map_function(::Type{BT}, func) where {BT} Return the image of `func` through the inverse of the adjoint of the linear map -`A` defined in [`Variable.SetMapBridge`](@ref) and -[`Constraint.SetMapBridge`](@ref). This is used for getting the -[`MOI.ConstraintDual`](@ref) of variable bridges and setting the -[`MOI.ConstraintDualStart`](@ref) of constraint bridges. +`A` defined in [`Variable.SetMapBridge`](@ref) and [`Constraint.SetMapBridge`](@ref). + +This function is used for getting the [`MOI.ConstraintDual`](@ref) of variable +bridges and setting the [`MOI.ConstraintDualStart`](@ref) of constraint bridges. + +If the linear map `A` is not invertible, the error [`MapNotInvertible`](@ref) is +thrown. + +The method can alternatively be defined on the bridge type. This legacy +interface is kept for backward compatibility. """ -function inverse_adjoint_map_function end +function inverse_adjoint_map_function(bridge::AbstractBridge, func) + return inverse_adjoint_map_function(typeof(bridge), func) +end diff --git a/test/Bridges/set_map.jl b/test/Bridges/set_map.jl new file mode 100644 index 0000000000..f4f600c60d --- /dev/null +++ b/test/Bridges/set_map.jl @@ -0,0 +1,185 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +# Test with a bridge for which the map is defined on the bridge value and not +# the bridge type +module TestSetMapBridge + +using Test + +import MathOptInterface as MOI + +@enum(ErrType, NONE, NOT_INVERTIBLE, OTHER) + +# Constraints `[f[2], f[1]]` if `swap` and otherwise `f` to `Nonnegatives` +struct SwapSet <: MOI.AbstractVectorSet + swap::Bool + err::ErrType +end + +MOI.dimension(::SwapSet) = 2 + +struct SwapBridge{T} <: MOI.Bridges.Constraint.SetMapBridge{ + T, + MOI.Nonnegatives, + SwapSet, + MOI.VectorOfVariables, + MOI.VectorOfVariables, +} + constraint::MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Nonnegatives} + set::SwapSet +end + +function MOI.Bridges.Constraint.bridge_constraint( + ::Type{SwapBridge{T}}, + model::MOI.ModelLike, + func::MOI.VectorOfVariables, + set::SwapSet, +) where {T} + ci = MOI.add_constraint( + model, + MOI.VectorOfVariables(swap(func.variables, set.swap)), + MOI.Nonnegatives(2), + ) + return SwapBridge{T}(ci, set) +end + +function MOI.Bridges.map_set(bridge::SwapBridge, set::SwapSet) + if set.swap != bridge.set.swap + error("Cannot change swap set") + end + return MOI.Nonnegatives(2) +end + +MOI.Bridges.inverse_map_set(bridge::SwapBridge, ::MOI.Nonnegatives) = bridge.set + +function MOI.Bridges.map_function(bridge::SwapBridge, func) + return swap(func, bridge.set.swap) +end + +function MOI.Bridges.inverse_map_function(bridge::SwapBridge, func) + if bridge.set.err == NONE + return swap(func, bridge.set.swap) + elseif bridge.set.err == NOT_INVERTIBLE + throw(MOI.Bridges.MapNotInvertible("no luck")) + else + error() + end +end + +function MOI.Bridges.adjoint_map_function(bridge::SwapBridge, func) + return swap(func, bridge.set.swap) +end + +function MOI.Bridges.inverse_adjoint_map_function(bridge::SwapBridge, func) + if bridge.set.err == NONE + return swap(func, bridge.set.swap) + elseif bridge.set.err == NOT_INVERTIBLE + throw(MOI.Bridges.MapNotInvertible("no luck")) + else + error() + end +end + +swap(x, swap::Bool) = swap ? [x[2], x[1]] : x + +function swap(f::MOI.VectorOfVariables, do_swap::Bool) + return MOI.VectorOfVariables(swap(f.variables, do_swap)) +end + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_other_error() + model = MOI.Bridges.Constraint.SingleBridgeOptimizer{SwapBridge{Float64}}( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + x = MOI.add_variables(model, 2) + func = MOI.VectorOfVariables(x) + ci = MOI.add_constraint(model, func, SwapSet(true, OTHER)) + @test_throws( + ErrorException(""), + MOI.get(model, MOI.ConstraintFunction(), ci), + ) + MOI.set(model, MOI.ConstraintPrimalStart(), ci, ones(2)) + @test_throws( + ErrorException(""), + MOI.get(model, MOI.ConstraintPrimalStart(), ci), + ) + @test_throws( + ErrorException(""), + MOI.set(model, MOI.ConstraintDualStart(), ci, ones(2)), + ) + return +end +function test_not_invertible() + model = MOI.Bridges.Constraint.SingleBridgeOptimizer{SwapBridge{Float64}}( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + x = MOI.add_variables(model, 2) + func = MOI.VectorOfVariables(x) + ci = MOI.add_constraint(model, func, SwapSet(true, NOT_INVERTIBLE)) + @test_throws( + ErrorException("Cannot change swap set"), + MOI.set(model, MOI.ConstraintSet(), ci, SwapSet(false, NOT_INVERTIBLE)), + ) + @test_throws( + MOI.GetAttributeNotAllowed( + MOI.ConstraintFunction(), + "Cannot get `MathOptInterface.ConstraintFunction()` as the constraint is reformulated through a linear transformation that is not invertible. no luck", + ), + MOI.get(model, MOI.ConstraintFunction(), ci), + ) + MOI.set(model, MOI.ConstraintPrimalStart(), ci, ones(2)) + @test_throws( + MOI.GetAttributeNotAllowed( + MOI.ConstraintPrimalStart(), + "Cannot get `MathOptInterface.ConstraintPrimalStart()` as the constraint is reformulated through a linear transformation that is not invertible. no luck", + ), + MOI.get(model, MOI.ConstraintPrimalStart(), ci), + ) + @test_throws( + MOI.SetAttributeNotAllowed( + MOI.ConstraintDualStart(), + "Cannot get `MathOptInterface.ConstraintDualStart()` as the constraint is reformulated through a linear transformation that is not invertible. no luck", + ), + MOI.set(model, MOI.ConstraintDualStart(), ci, ones(2)), + ) + return +end + +function test_runtests() + for do_swap in [false, true] + MOI.Bridges.runtests( + SwapBridge, + model -> begin + x = MOI.add_variables(model, 2) + func = MOI.VectorOfVariables(x) + set = SwapSet(do_swap, NONE) + MOI.add_constraint(model, func, set) + end, + model -> begin + x = MOI.add_variables(model, 2) + func = MOI.VectorOfVariables(swap(x, do_swap)) + set = MOI.Nonnegatives(2) + MOI.add_constraint(model, func, set) + end, + ) + end + return +end + +end # module + +TestSetMapBridge.runtests()