diff --git a/src/Bridges/Constraint/set_map.jl b/src/Bridges/Constraint/set_map.jl index a9720c1c55..d023223d34 100644 --- a/src/Bridges/Constraint/set_map.jl +++ b/src/Bridges/Constraint/set_map.jl @@ -92,6 +92,21 @@ function MOI.get( return MOIU.convert_approx(G, func) end +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintFunction, + bridge::SetMapBridge{T,S2,S1,F,G}, + func::G, +) where {T,S2,S1,F,G} + MOI.set( + model, + attr, + bridge.constraint, + MOIB.map_function(typeof(bridge), func), + ) + return +end + function MOI.get( model::MOI.ModelLike, attr::MOI.ConstraintSet, @@ -164,14 +179,34 @@ function MOI.set( return end +# By linearity of the map, we can just change the constant/coefficient +function _map_change(::Type{BT}, change::MOI.ScalarConstantChange) where {BT} + constant = MOIB.map_function(BT, change.new_constant) + return MOI.ScalarConstantChange(constant) +end +function _map_change(::Type{BT}, change::MOI.VectorConstantChange) where {BT} + constant = MOIB.map_function(BT, change.new_constant) + return MOI.VectorConstantChange(constant) +end +function _map_change(::Type{BT}, change::MOI.ScalarCoefficientChange) where {BT} + coefficient = MOIB.map_function(BT, change.new_coefficient) + return MOI.ScalarCoefficientChange(change.variable, coefficient) +end +function _map_change(::Type{BT}, change::MOI.MultirowChange) where {BT} + # It is important here that `change.new_coefficients` contains + # the complete new sparse column associated to the variable. + # Calling modify twice with part of the column won't work since + # the linear map might reset all the column each time. + coefficients = MOIB.map_function(BT, change.new_coefficients) + return MOI.MultirowChange(change.variable, coefficients) +end + function MOI.modify( model::MOI.ModelLike, bridge::SetMapBridge, - change::MOI.VectorConstantChange, + change::MOI.AbstractFunctionModification, ) - # By linearity of the map, we can just change the constant - constant = MOIB.map_function(typeof(bridge), change.new_constant) - MOI.modify(model, bridge.constraint, MOI.VectorConstantChange(constant)) + MOI.modify(model, bridge.constraint, _map_change(typeof(bridge), change)) return end diff --git a/src/Bridges/Constraint/single_bridge_optimizer.jl b/src/Bridges/Constraint/single_bridge_optimizer.jl index 48b1987e1e..caa76d8612 100644 --- a/src/Bridges/Constraint/single_bridge_optimizer.jl +++ b/src/Bridges/Constraint/single_bridge_optimizer.jl @@ -103,3 +103,5 @@ function MOIB.bridge_type( end MOIB.bridging_cost(::SingleBridgeOptimizer, args...) = 1.0 + +MOIB.recursive_model(b::SingleBridgeOptimizer) = b.model diff --git a/src/Bridges/Objective/single_bridge_optimizer.jl b/src/Bridges/Objective/single_bridge_optimizer.jl index 048c220192..c553585590 100644 --- a/src/Bridges/Objective/single_bridge_optimizer.jl +++ b/src/Bridges/Objective/single_bridge_optimizer.jl @@ -70,3 +70,5 @@ function MOIB.bridge_type( ) where {BT} return BT end + +MOIB.recursive_model(b::SingleBridgeOptimizer) = b.model diff --git a/src/Bridges/Variable/set_map.jl b/src/Bridges/Variable/set_map.jl index 86bc8e6b18..0f86f7fa96 100644 --- a/src/Bridges/Variable/set_map.jl +++ b/src/Bridges/Variable/set_map.jl @@ -23,13 +23,21 @@ bridge. """ abstract type SetMapBridge{T,S1,S2} <: AbstractBridge end +function _add_constrained_var(model, set::MOI.AbstractScalarSet) + return MOI.add_constrained_variable(model, set) +end + +function _add_constrained_var(model, set::MOI.AbstractVectorSet) + return MOI.add_constrained_variables(model, set) +end + function bridge_constrained_variable( BT::Type{<:SetMapBridge{T,S1,S2}}, model::MOI.ModelLike, set::S2, ) where {T,S1,S2} variables, constraint = - MOI.add_constrained_variables(model, MOIB.inverse_map_set(BT, set)) + _add_constrained_var(model, MOIB.inverse_map_set(BT, set)) return BT(variables, constraint) end @@ -88,7 +96,17 @@ function MOI.get( end # References -function MOI.delete(model::MOI.ModelLike, bridge::SetMapBridge) +function MOI.delete( + model::MOI.ModelLike, + bridge::SetMapBridge{T,S1,S2}, +) where {T,S1,S2<:MOI.AbstractScalarSet} + MOI.delete(model, bridge.variable) + return +end +function MOI.delete( + model::MOI.ModelLike, + bridge::SetMapBridge{T,S1,S2}, +) where {T,S1,S2<:MOI.AbstractVectorSet} MOI.delete(model, bridge.variables) return end @@ -104,6 +122,17 @@ function MOI.get( return MOIB.map_set(typeof(bridge), set) end +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintSet, + bridge::SetMapBridge{T,S1}, + set::S1, +) where {T,S1} + mapped = MOIB.inverse_map_set(typeof(bridge), set) + MOI.set(model, attr, bridge.constraint, mapped) + return +end + function MOI.get( model::MOI.ModelLike, attr::MOI.ConstraintPrimal, @@ -164,6 +193,13 @@ function MOIB.bridged_function( return convert(MOI.ScalarAffineFunction{T}, func) end +function unbridged_map(bridge::SetMapBridge{T}, vi::MOI.VariableIndex) where {T} + F = MOI.ScalarAffineFunction{T} + func = MOI.SingleVariable(vi) + mapped = MOIB.inverse_map_function(typeof(bridge), func) + return Pair{MOI.VariableIndex,F}[bridge.variable=>mapped] +end + function unbridged_map( bridge::SetMapBridge{T}, vis::Vector{MOI.VariableIndex}, diff --git a/src/Bridges/Variable/single_bridge_optimizer.jl b/src/Bridges/Variable/single_bridge_optimizer.jl index 01b3bd35f6..9f216ee055 100644 --- a/src/Bridges/Variable/single_bridge_optimizer.jl +++ b/src/Bridges/Variable/single_bridge_optimizer.jl @@ -82,3 +82,5 @@ function MOIB.bridge_type( end MOIB.bridging_cost(::SingleBridgeOptimizer, args...) = 1.0 + +MOIB.recursive_model(b::SingleBridgeOptimizer) = b.model diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 11e5c0e7ed..9be95eefec 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -15,6 +15,8 @@ abstract type AbstractBridgeOptimizer <: MOI.AbstractOptimizer end # AbstractBridgeOptimizer interface +function recursive_model end + function supports_constraint_bridges end """ @@ -293,6 +295,33 @@ function call_in_context( end end +function call_in_context( + f::F, + b::AbstractBridgeOptimizer, + index::MOI.Index, + attr::MOI.AnyAttribute, + args::Vararg{Any,N}, +) where {F<:Union{typeof(MOI.get),typeof(MOI.set)},N} + return call_in_context( + b, + index, + bridge -> f(recursive_model(b), attr, bridge, args...), + ) +end + +function call_in_context( + f::F, + b::AbstractBridgeOptimizer, + index::MOI.Index, + args::Vararg{Any,N}, +) where {F<:Function,N} + return call_in_context( + b, + index, + bridge -> f(recursive_model(b), bridge, args...), + ) +end + function _functionize_bridge(b::AbstractBridgeOptimizer, bridge_type) func, name = _func_name(bridge_type) return error( @@ -451,11 +480,7 @@ function _delete_variables_in_vector_of_variables_constraint( i = findfirst(isequal(vi), variables) if i !== nothing if MOI.supports_dimension_update(S) - call_in_context( - b, - ci, - bridge -> MOI.delete(b, bridge, IndexInVector(i)), - ) + call_in_context(MOI.delete, b, ci, IndexInVector(i)) else MOIU.throw_delete_variable_in_vov(vi) end @@ -504,7 +529,7 @@ function MOI.delete(b::AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}) end if all(vi -> is_bridged(b, vi), vis) && Variable.has_keys(Variable.bridges(b), vis) - call_in_context(b, first(vis), bridge -> MOI.delete(b, bridge)) + call_in_context(MOI.delete, b, first(vis)) b.name_to_var = nothing for vi in vis delete!(b.var_to_name, vi) @@ -533,16 +558,12 @@ function MOI.delete(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) if MOI.supports_dimension_update( Variable.constrained_set(Variable.bridges(b), vi), ) - call_in_context( - b, - vi, - bridge -> MOI.delete(b, bridge, _index(b, vi)...), - ) + call_in_context(MOI.delete, b, vi, _index(b, vi)...) else MOIU.throw_delete_variable_in_vov(vi) end else - call_in_context(b, vi, bridge -> MOI.delete(b, bridge)) + call_in_context(MOI.delete, b, vi) ci = Variable.constraint(Variable.bridges(b), vi) b.name_to_con = nothing delete!(b.con_to_name, ci) @@ -571,7 +592,7 @@ function MOI.delete(b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex) Variable.call_in_context( Variable.bridges(b), ci, - () -> MOI.delete(b, br), + () -> MOI.delete(recursive_model(b), br), ) b.name_to_con = nothing delete!(b.con_to_name, ci) @@ -899,13 +920,19 @@ struct ObjectiveFunctionValue{F<:MOI.AbstractScalarFunction} result_index::Int end +# `recursive_model(b::Objective.SingleBridgeOptimizer)` returns +# `b.model` so any model should implement `ObjectiveFunctionValue`. +function MOI.get(model::MOI.ModelLike, attr::ObjectiveFunctionValue) + return MOI.get(model, MOI.ObjectiveValue(attr.result_index)) +end + function MOI.get( b::AbstractBridgeOptimizer, attr::ObjectiveFunctionValue{F}, -) where {F} +) where {F<:MOI.AbstractScalarFunction} # Need `<:` to avoid ambiguity obj_attr = MOI.ObjectiveFunction{F}() if is_bridged(b, obj_attr) - return MOI.get(b, attr, bridge(b, obj_attr)) + return MOI.get(recursive_model(b), attr, bridge(b, obj_attr)) else return MOI.get(b.model, MOI.ObjectiveValue(attr.result_index)) end @@ -934,7 +961,7 @@ end function MOI.get(b::AbstractBridgeOptimizer, attr::MOI.ObjectiveFunction) value = if is_bridged(b, attr) - MOI.get(b, attr, bridge(b, attr)) + MOI.get(recursive_model(b), attr, bridge(b, attr)) else MOI.get(b.model, attr) end @@ -952,7 +979,7 @@ function MOI.set( _delete_objective_bridges(b) else for bridge in values(Objective.bridges(b)) - MOI.set(b, attr, bridge, value) + MOI.set(recursive_model(b), attr, bridge, value) end end end @@ -960,7 +987,7 @@ function MOI.set( end function _bridge_objective(b, BridgeType, func) - bridge = Objective.bridge_objective(BridgeType, b, func) + bridge = Objective.bridge_objective(BridgeType, recursive_model(b), func) Objective.add_key_for_bridge(Objective.bridges(b), bridge, func) return end @@ -1013,7 +1040,7 @@ function MOI.modify( if is_bridged(b, change) modify_bridged_change(b, obj, change) elseif is_bridged(b, obj) - MOI.modify(b, bridge(b, obj), change) + MOI.modify(recursive_model(b), bridge(b, obj), change) else MOI.modify(b.model, obj, change) end @@ -1036,11 +1063,7 @@ function MOI.get( index::MOI.VariableIndex, ) if is_bridged(b, index) - value = call_in_context( - b, - index, - bridge -> MOI.get(b, attr, bridge, _index(b, index)...), - ) + value = call_in_context(MOI.get, b, index, attr, _index(b, index)...) else value = MOI.get(b.model, attr, index) end @@ -1075,11 +1098,7 @@ function MOI.set( ) value = bridged_function(b, value) if is_bridged(b, index) - call_in_context( - b, - index, - bridge -> MOI.set(b, attr, bridge, value, _index(b, index)...), - ) + call_in_context(MOI.set, b, index, attr, value, _index(b, index)...) else MOI.set(b.model, attr, index, value) end @@ -1128,7 +1147,7 @@ function MOI.get( if is_variable_bridged(b, ci) return Variable.function_for(Variable.bridges(b), ci) else - func = call_in_context(b, ci, br -> MOI.get(b, attr, br)) + func = call_in_context(MOI.get, b, ci, attr) return unbridged_constraint_function(b, func) end else @@ -1153,7 +1172,7 @@ function MOI.get( ) if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) - return call_in_context(b, ci, bridge -> MOI.get(b, attr, bridge)) + return call_in_context(MOI.get, b, ci, attr) else return MOI.get(b.model, attr, ci) end @@ -1180,7 +1199,7 @@ function MOI.get( ) if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) - set = call_in_context(b, ci, bridge -> MOI.get(b, attr, bridge)) + set = call_in_context(MOI.get, b, ci, attr) else set = MOI.get(b.model, attr, ci) end @@ -1188,11 +1207,7 @@ function MOI.get( # The function constant of the bridged function was moved to the set, # we need to remove it. if is_bridged(b, ci) - func = call_in_context( - b, - ci, - bridge -> MOI.get(b, MOI.ConstraintFunction(), bridge), - ) + func = call_in_context(MOI.get, b, ci, MOI.ConstraintFunction()) else func = MOI.get(b.model, MOI.ConstraintFunction(), ci) end @@ -1209,7 +1224,7 @@ function MOI.get( ) if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) - func = call_in_context(b, ci, bridge -> MOI.get(b, attr, bridge)) + func = call_in_context(MOI.get, b, ci, attr) else func = MOI.get(b.model, attr, ci) end @@ -1242,7 +1257,7 @@ function _set_substituted( ) if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) - call_in_context(b, ci, bridge -> MOI.set(b, attr, bridge, value)) + call_in_context(MOI.set, b, ci, attr, value) else MOI.set(b.model, attr, ci, value) end @@ -1424,7 +1439,7 @@ function MOI.supports_constraint( end function add_bridged_constraint(b, BridgeType, f, s) - bridge = Constraint.bridge_constraint(BridgeType, b, f, s) + bridge = Constraint.bridge_constraint(BridgeType, recursive_model(b), f, s) ci = Constraint.add_key_for_bridge(Constraint.bridges(b), bridge, f, s) Variable.register_context(Variable.bridges(b), ci) return ci @@ -1607,7 +1622,7 @@ function MOI.modify( modify_bridged_change(b, ci, change) else if is_bridged(b, ci) - call_in_context(b, ci, bridge -> MOI.modify(b, bridge, change)) + call_in_context(MOI.modify, b, ci, change) else MOI.modify(b.model, ci, change) end @@ -1702,7 +1717,11 @@ function MOI.add_constrained_variable( BridgeType = Variable.concrete_bridge_type(b, typeof(set)) return Variable.add_key_for_bridge( Variable.bridges(b), - () -> Variable.bridge_constrained_variable(BridgeType, b, set), + () -> Variable.bridge_constrained_variable( + BridgeType, + recursive_model(b), + set, + ), set, ) else diff --git a/src/Bridges/lazy_bridge_optimizer.jl b/src/Bridges/lazy_bridge_optimizer.jl index 2b594d0750..fbdd4a2e9c 100644 --- a/src/Bridges/lazy_bridge_optimizer.jl +++ b/src/Bridges/lazy_bridge_optimizer.jl @@ -513,6 +513,8 @@ function bridging_cost(b::LazyBridgeOptimizer, args...) return bridging_cost(b.graph, node(b, args...)) end +recursive_model(b::LazyBridgeOptimizer) = b + function MOI.compute_conflict!(model::LazyBridgeOptimizer) return MOI.compute_conflict!(model.model) end diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index b2c6875875..edef17256d 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -703,6 +703,72 @@ function test_get_ObjectiveFunctionType() return end +include("identity_bridge.jl") + +function test_recursive_model_variable(::Type{T} = Int) where {T} + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + BT = IdentityBridges.VariableBridge{T} + b = MOI.Bridges.Variable.SingleBridgeOptimizer{BT}(model) + x, cx = MOI.add_constrained_variable(b, MOI.EqualTo(one(T))) + @test MOI.Bridges.is_bridged(b, x) + @test MOI.Bridges.is_bridged(b, cx) + @test MOI.get(b, MOI.ConstraintFunction(), cx) == MOI.SingleVariable(x) + @test MOI.get(b, MOI.ConstraintSet(), cx) == MOI.EqualTo(one(T)) + MOI.set(b, MOI.ConstraintSet(), cx, MOI.EqualTo(zero(T))) + @test MOI.get(b, MOI.ConstraintSet(), cx) == MOI.EqualTo(zero(T)) + @test MOI.is_valid(b, x) + MOI.delete(b, x) + @test !MOI.is_valid(b, x) +end + +function test_recursive_model_constraint(::Type{T} = Int) where {T} + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + BT = IdentityBridges.ConstraintBridge{T} + b = MOI.Bridges.Constraint.SingleBridgeOptimizer{BT}(model) + x = MOI.add_variable(b) + fx = MOI.SingleVariable(x) + func = one(T) * fx + set = MOI.EqualTo(zero(T)) + c = MOI.add_constraint(b, func, set) + @test MOI.Bridges.is_bridged(b, c) + @test MOI.get(b, MOI.ConstraintFunction(), c) ≈ func + new_func = T(2) * fx + MOI.set(b, MOI.ConstraintFunction(), c, new_func) + @test MOI.get(b, MOI.ConstraintFunction(), c) == new_func + MOI.modify(b, c, MOI.ScalarCoefficientChange(x, T(3))) + @test MOI.get(b, MOI.ConstraintFunction(), c) ≈ T(3) * fx + MOI.modify(b, c, MOI.ScalarConstantChange(T(-1))) + @test MOI.get(b, MOI.ConstraintFunction(), c) ≈ T(3) * fx + T(-1) + @test MOI.get(b, MOI.ConstraintSet(), c) == set + new_set = MOI.EqualTo(one(T)) + MOI.set(b, MOI.ConstraintSet(), c, new_set) + @test MOI.get(b, MOI.ConstraintSet(), c) == new_set + MOI.set(b, MOI.ConstraintDualStart(), c, one(T)) + @test MOI.get(b, MOI.ConstraintDualStart(), c) == one(T) + @test MOI.is_valid(b, c) + MOI.delete(b, c) + @test !MOI.is_valid(b, c) +end + +function test_recursive_model_objective(::Type{T} = Int) where {T} + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + BT = IdentityBridges.ObjectiveBridge{T} + b = MOI.Bridges.Objective.SingleBridgeOptimizer{BT}(model) + x = MOI.add_variable(b) + fx = MOI.SingleVariable(x) + func = one(T) * fx + MOI.set(b, MOI.ObjectiveSense(), MOI.MIN_SENSE) + @test !MOI.Bridges.is_objective_bridged(b) + attr = MOI.ObjectiveFunction{typeof(func)}() + MOI.set(b, attr, func) + @test MOI.Bridges.is_objective_bridged(b) + @test MOI.get(b, attr) ≈ func + attr = MOI.ObjectiveFunction{typeof(fx)}() + MOI.set(b, attr, fx) + @test !MOI.Bridges.is_objective_bridged(b) + @test MOI.get(b, attr) ≈ fx +end + end # module TestBridgeOptimizer.runtests() diff --git a/test/Bridges/identity_bridge.jl b/test/Bridges/identity_bridge.jl new file mode 100644 index 0000000000..9a365f0e78 --- /dev/null +++ b/test/Bridges/identity_bridge.jl @@ -0,0 +1,92 @@ +# Dummy bridges used for testing +module IdentityBridges + +import MathOptInterface +const MOI = MathOptInterface +const MOIB = MOI.Bridges + +const F{T} = MOI.ScalarAffineFunction{T} +const S{T} = MOI.EqualTo{T} + +struct VariableBridge{T} <: MOIB.Variable.SetMapBridge{T,S{T},S{T}} + variable::MOI.VariableIndex + constraint::MOI.ConstraintIndex{MOI.SingleVariable,S{T}} +end + +struct ConstraintBridge{T} <: + MOIB.Constraint.SetMapBridge{T,S{T},S{T},F{T},F{T}} + constraint::MOI.ConstraintIndex{F{T},S{T}} +end + +const IdentityBridge{T} = Union{VariableBridge{T},ConstraintBridge{T}} + +MOIB.map_set(::Type{<:IdentityBridge}, set::S) = set +MOIB.inverse_map_set(::Type{<:IdentityBridge}, set::S) = set +MOIB.map_function(::Type{<:IdentityBridge}, func) = func +MOIB.inverse_map_function(::Type{<:IdentityBridge}, func) = func +MOIB.adjoint_map_function(::Type{<:IdentityBridge}, func) = func +MOIB.inverse_adjoint_map_function(::Type{<:IdentityBridge}, func) = func + +struct ObjectiveBridge{T} <: MOIB.Objective.AbstractBridge end + +function MOIB.Objective.bridge_objective( + ::Type{ObjectiveBridge{T}}, + model::MOI.ModelLike, + func::F{T}, +) where {T} + MOI.set(model, MOI.ObjectiveFunction{F}(), func) + return ObjectiveBridge{T}() +end + +function MOIB.Objective.supports_objective_function( + ::Type{ObjectiveBridge{T}}, + ::Type{F{T}}, +) where {T} + return true +end + +function MOIB.added_constrained_variable_types(::Type{<:ObjectiveBridge}) + return Tuple{DataType}[] +end + +function MOIB.added_constraint_types(::Type{<:ObjectiveBridge}) + return Tuple{DataType,DataType}[] +end + +function MOIB.set_objective_function_type(::Type{ObjectiveBridge{T}}) where {T} + return F{T} +end + +# Attributes, Bridge acting as a model +function MOI.get(::ObjectiveBridge, ::MOI.NumberOfVariables) + return 0 +end + +function MOI.get(::ObjectiveBridge, ::MOI.ListOfVariableIndices) + return MOI.VariableIndex[] +end + +# No variables or constraints are created in this bridge so there is nothing to +# delete. +MOI.delete(model::MOI.ModelLike, bridge::ObjectiveBridge) = nothing + +function MOI.set( + ::MOI.ModelLike, + ::MOI.ObjectiveSense, + ::ObjectiveBridge, + ::MOI.OptimizationSense, +) + # `ObjectiveBridge` is sense agnostic, therefore, we don't need to change + # anything. + return +end + +function MOI.get( + model::MOI.ModelLike, + attr::Union{MOIB.ObjectiveFunctionValue{F{T}},MOI.ObjectiveFunction{F{T}}}, + ::ObjectiveBridge{T}, +) where {T} + return MOI.get(model, attr) +end + +end