diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index 3f8e68e404..23936738be 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -14,4 +14,8 @@ include("map.jl") # Bridge optimizer bridging a specific variable bridge include("single_bridge_optimizer.jl") +# Variable bridges +include("zeros.jl") +const Zeros{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{ZerosBridge{T}, OT} + end diff --git a/src/Bridges/Variable/zeros.jl b/src/Bridges/Variable/zeros.jl new file mode 100644 index 0000000000..906795d401 --- /dev/null +++ b/src/Bridges/Variable/zeros.jl @@ -0,0 +1,68 @@ +""" + ZerosBridge{T} <: Bridges.Variable.AbstractBridge + +Transforms constrained variables in [`MathOptInterface.Zeros`](@ref) to zeros, +which ends up creating no variables in the underlying model. +The bridged variables are therefore similar to parameters with zero values. +Parameters with non-zero value can be created with constrained variables in +[`MOI.EqualTo`](@ref) by combining a [`VectorizeBridge`](@ref) and this bridge. +The functions cannot be unbridged, given a function, we cannot determine, if +the bridged variables were used. +The dual values cannot be determined by the bridge but they can be determined +by the bridged optimizer using [`MathOptInterface.Utilities.get_fallback`](@ref) +if a `CachingOptimizer` is used (since `ConstraintFunction` cannot be got +as functions cannot be unbridged). +""" +struct ZerosBridge{T} <: AbstractBridge + n::Int # Number of variables +end +function bridge_constrained_variable(::Type{ZerosBridge{T}}, + model::MOI.ModelLike, + set::MOI.Zeros) where T + return ZerosBridge{T}(MOI.dimension(set)) +end + +function supports_constrained_variable( + ::Type{<:ZerosBridge}, ::Type{MOI.Zeros}) + return true +end +function MOIB.added_constrained_variable_types(::Type{<:ZerosBridge}) + return Tuple{DataType}[] +end +function MOIB.added_constraint_types(::Type{<:ZerosBridge}) + return Tuple{DataType, DataType}[] +end + +# Attributes, Bridge acting as a model +MOI.get(bridge::ZerosBridge, ::MOI.NumberOfVariables) = 0 +function MOI.get(bridge::ZerosBridge, ::MOI.ListOfVariableIndices) + return MOI.VariableIndex[] +end + +# References +function MOI.delete(::MOI.ModelLike, ::ZerosBridge) end + +# Attributes, Bridge acting as a constraint + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, + bridge::ZerosBridge) + return MOI.Zeros(bridge.n) +end + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintPrimal, + bridge::ZerosBridge{T}) where T + return zeros(T, bridge.n) +end + +function MOI.get(::MOI.ModelLike, ::MOI.VariablePrimal, + ::ZerosBridge{T}, ::IndexInVector) where T + return zero(T) +end + +function MOIB.bridged_function(::ZerosBridge{T}, ::IndexInVector) where T + return zero(MOI.ScalarAffineFunction{T}) +end +function unbridged_map(::ZerosBridge, ::MOI.VariableIndex, + ::IndexInVector) + return nothing +end diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 493314b3f3..78a0ed38ec 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -931,6 +931,12 @@ function bridged_function(b::AbstractBridgeOptimizer, end return func end +# Shortcut to avoid `Variable.throw_if_cannot_unbridge(Variable.bridges(b))` +function bridge_function( + ::AbstractBridgeOptimizer, value::MOIU.ObjectOrArrayWithoutIndex) + return value +end + """ unbridged_variable_function(b::AbstractBridgeOptimizer, @@ -975,6 +981,11 @@ function unbridged_function(bridge::AbstractBridgeOptimizer, func::Union{MOI.SingleVariable, MOI.VectorOfVariables}) return func # bridged variables are not allowed in non-bridged constraints end +# Shortcut to avoid `Variable.throw_if_cannot_unbridge(Variable.bridges(b))` +function unbridged_function( + ::AbstractBridgeOptimizer, value::MOIU.ObjectOrArrayWithoutIndex) + return value +end """ unbridged_constraint_function( diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 25e4c9230a..aed2f45ae1 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -106,12 +106,10 @@ or submittable value. """ function substitute_variables end -function substitute_variables( - ::Function, x::Union{Number, Enum, AbstractArray{<:Union{Number, Enum}}}) - return x -end +const ObjectWithoutIndex = Union{Number, Enum, MOI.AnyAttribute, MOI.AbstractSet} +const ObjectOrArrayWithoutIndex = Union{ObjectWithoutIndex, AbstractArray{<:ObjectWithoutIndex}} -substitute_variables(::Function, set::MOI.AbstractSet) = set +substitute_variables(::Function, x::ObjectOrArrayWithoutIndex) = x function substitute_variables(variable_map::Function, term::MOI.ScalarQuadraticTerm{T}) where T diff --git a/src/attributes.jl b/src/attributes.jl index be1520f1a6..ee25aecd88 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -34,6 +34,8 @@ Abstract supertype for attribute objects that can be used to set or get attribut """ abstract type AbstractConstraintAttribute end +# Attributes should not contain any `VariableIndex` or `ConstraintIndex` as the +# set is passed unmodifed during `copy_to`. const AnyAttribute = Union{AbstractOptimizerAttribute, AbstractModelAttribute, AbstractVariableAttribute, AbstractConstraintAttribute} # This allows to use attributes in broadcast calls without the need to diff --git a/test/Bridges/Bridges.jl b/test/Bridges/Bridges.jl index 040b21f647..d6a5bc3e90 100644 --- a/test/Bridges/Bridges.jl +++ b/test/Bridges/Bridges.jl @@ -6,6 +6,9 @@ end @testset "LazyBridgeOptimizer" begin include("lazy_bridge_optimizer.jl") end +@testset "Variable bridges" begin + include("Variable/Variable.jl") +end @testset "Constraint bridges" begin include("Constraint/Constraint.jl") end diff --git a/test/Bridges/Variable/Variable.jl b/test/Bridges/Variable/Variable.jl index f2a99d8443..5a27995233 100644 --- a/test/Bridges/Variable/Variable.jl +++ b/test/Bridges/Variable/Variable.jl @@ -1,3 +1,7 @@ +using Test @testset "Map" begin include("map.jl") end +@testset "Zeros" begin + include("zeros.jl") +end diff --git a/test/Bridges/Variable/zeros.jl b/test/Bridges/Variable/zeros.jl new file mode 100644 index 0000000000..cd4b381613 --- /dev/null +++ b/test/Bridges/Variable/zeros.jl @@ -0,0 +1,155 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("../utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) +config = MOIT.TestConfig() + +bridged_mock = MOIB.Variable.Zeros{Float64}(mock) + +x, cx = MOI.add_constrained_variable(bridged_mock, MOI.GreaterThan(0.0)) +MOI.set(bridged_mock, MOI.VariableName(), x, "x") +MOI.set(bridged_mock, MOI.ConstraintName(), cx, "cx") +yz, cyz = MOI.add_constrained_variables(bridged_mock, MOI.Zeros(2)) +MOI.set(bridged_mock, MOI.VariableName(), yz, ["y", "z"]) +MOI.set(bridged_mock, MOI.ConstraintName(), cyz, "cyz") +y, z = yz +fx = MOI.SingleVariable(x) +fy = MOI.SingleVariable(y) +fz = MOI.SingleVariable(z) + +@testset "SingleVariable objective" begin + err = ErrorException("Using bridged variable in `SingleVariable` function.") + @test_throws err MOI.set(bridged_mock, MOI.ObjectiveFunction{typeof(fy)}(), fy) + MOI.set(bridged_mock, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(bridged_mock, MOI.ObjectiveFunction{typeof(fx)}(), fx) + @test MOI.get(bridged_mock, MOI.ObjectiveFunction{typeof(fx)}()) == fx +end + +# Test before adding affine constraints are affine expressions cannot be +# unbridged when `Variable.ZerosBridge` is used. +@testset "Test bridged model" begin + s = """ + variables: x, y, z + cx: x >= 0.0 + cyz: [y, z] in MathOptInterface.Zeros(2) + minobjective: x + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(bridged_mock, model, ["x", "y", "z"], ["cx", "cyz"]) +end + +c1, c2 = MOI.add_constraints( + bridged_mock, [1.0fy + 1.0fz, 1.0fx + 1.0fy + 1.0fz], + [MOI.EqualTo(0.0), MOI.GreaterThan(1.0)] +) +MOI.set(bridged_mock, MOI.ConstraintName(), c1, "con1") +MOI.set(bridged_mock, MOI.ConstraintName(), c2, "con2") +MOI.set(bridged_mock, MOI.ObjectiveSense(), MOI.MIN_SENSE) +obj = 1.0fx - 1.0fy - 1.0fz +MOI.set(bridged_mock, MOI.ObjectiveFunction{typeof(obj)}(), obj) + +@test MOIB.Variable.unbridged_map(MOIB.bridge(bridged_mock, y), y, MOIB.Variable.IndexInVector(1)) === nothing +@test MOIB.Variable.unbridged_map(MOIB.bridge(bridged_mock, z), z, MOIB.Variable.IndexInVector(2)) === nothing + +err = ErrorException( + "Cannot delete constraint index of bridged constrained variables. Delete" * + " the scalar variable or the vector of variables instead." +) +@test_throws err MOI.delete(bridged_mock, cyz) + +err = ErrorException( + "Cannot add two `VectorOfVariables`-in-`MathOptInterface.Zeros` on the" * + " same first variable MathOptInterface.VariableIndex(-1)." +) +@test_throws err MOI.add_constraint(bridged_mock, MOI.VectorOfVariables(yz), MOI.Zeros(2)) + +err = ErrorException( + "Cannot `VectorOfVariables`-in-`MathOptInterface.Zeros` for" * + " which some variables are bridged but not the first one" * + " `MathOptInterface.VariableIndex(12345679)`." +) +@test_throws err MOI.add_constraint(bridged_mock, MOI.VectorOfVariables([x, y]), MOI.Zeros(2)) + +err = ErrorException( + "Cannot unbridge function because some variables are bridged by" * + " variable bridges that do not support reverse mapping, e.g.," * + " `ZerosBridge`." +) +@test_throws err MOI.get(bridged_mock, MOI.ObjectiveFunction{typeof(obj)}()) +# With `c1`, the function does not contain any variable so it tests that it +# also throws an error even if it never calls `variable_unbridged_function`. +@test_throws err MOI.get(bridged_mock, MOI.ConstraintFunction(), c1) +@test_throws err MOI.get(bridged_mock, MOI.ConstraintFunction(), c2) + +err = ArgumentError( + "Variable bridge of type `MathOptInterface.Bridges.Variable.ZerosBridge{Float64}`" * + " does not support accessing the attribute `MathOptInterface.Test.UnknownVariableAttribute()`." +) +@test_throws err MOI.get(bridged_mock, MOIT.UnknownVariableAttribute(), y) + +@testset "Results" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, [1.0], + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) => 0.0, + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) => 1.0) + ) + MOI.optimize!(bridged_mock) + @test MOI.get(bridged_mock, MOI.VariablePrimal(), x) == 1.0 + @test MOI.get(bridged_mock, MOI.VariablePrimal(), y) == 0.0 + @test MOI.get(bridged_mock, MOI.VariablePrimal(), z) == 0.0 + + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cyz) == zeros(2) + + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cx) == 0.0 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), c1) == 0.0 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), c2) == 1.0 + + err = ArgumentError( + "Bridge of type `MathOptInterface.Bridges.Variable.ZerosBridge{Float64}`" * + " does not support accessing the attribute" * + " `MathOptInterface.ConstraintDual(1)`." + ) + @test_throws err MOI.get(bridged_mock, MOI.ConstraintDual(), cyz) +end + +@testset "Query" begin + @test MOI.get(bridged_mock, MOI.ConstraintFunction(), cyz).variables == yz + @test MOI.get(mock, MOI.NumberOfVariables()) == 1 + @test MOI.get(mock, MOI.ListOfVariableIndices()) == [x] + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 3 + @test MOI.get(bridged_mock, MOI.ListOfVariableIndices()) == [x, y, z] + @test MOI.get(mock, MOI.NumberOfConstraints{MOI.VectorOfVariables, MOI.Zeros}()) == 0 + @test MOI.get(bridged_mock, MOI.NumberOfConstraints{MOI.VectorOfVariables, MOI.Zeros}()) == 1 + @test MOI.get(bridged_mock, MOI.ListOfConstraintIndices{MOI.VectorOfVariables, MOI.Zeros}()) == [cyz] +end + +@testset "Test mock model" begin + s = """ + variables: x + cx: x >= 0.0 + con1: x + 0.0 == 0.0 + con2: x + 0.0 >= 1.0 + minobjective: x + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(mock, model, ["x"], ["cx", "con1", "con2"]) +end + +@testset "Delete" begin + test_delete_bridged_variables(bridged_mock, yz, MOI.Zeros, 3, ( + (MOI.SingleVariable, MOI.GreaterThan{Float64}, 1), + )) + @test MOI.is_valid(bridged_mock, x) + @test !MOI.is_valid(bridged_mock, y) + @test !MOI.is_valid(bridged_mock, z) +end diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index a3f45e2b78..d4fe80c14b 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -83,16 +83,16 @@ end c1 = MOI.add_constraint(model, f1, MOI.Interval(-1, 1)) @test MOI.get(model, MOI.ListOfConstraints()) == [(MOI.ScalarAffineFunction{Int},MOI.Interval{Int})] - test_noc(model, MOI.ScalarAffineFunction{Int}, MOI.GreaterThan{Int}, 0) - test_noc(model, MOI.ScalarAffineFunction{Int}, MOI.Interval{Int}, 1) + test_num_constraints(model, MOI.ScalarAffineFunction{Int}, MOI.GreaterThan{Int}, 0) + test_num_constraints(model, MOI.ScalarAffineFunction{Int}, MOI.Interval{Int}, 1) @test (@inferred MOI.get(model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Int},MOI.Interval{Int}}())) == [c1] f2 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2, -1], [x, y]), 2) c2 = MOI.add_constraint(model, f1, MOI.GreaterThan(-2)) @test MOI.get(model, MOI.ListOfConstraints()) == [(MOI.ScalarAffineFunction{Int},MOI.GreaterThan{Int}), (MOI.ScalarAffineFunction{Int},MOI.Interval{Int})] - test_noc(model, MOI.ScalarAffineFunction{Int}, MOI.GreaterThan{Int}, 1) - test_noc(model, MOI.ScalarAffineFunction{Int}, MOI.Interval{Int}, 1) + test_num_constraints(model, MOI.ScalarAffineFunction{Int}, MOI.GreaterThan{Int}, 1) + test_num_constraints(model, MOI.ScalarAffineFunction{Int}, MOI.Interval{Int}, 1) @test (@inferred MOI.get(model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Int},MOI.Interval{Int}}())) == [c1] @test (@inferred MOI.get(model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Int},MOI.GreaterThan{Int}}())) == [c2] @@ -100,8 +100,8 @@ end MOI.delete(model, c2) @test MOI.get(model, MOI.ListOfConstraints()) == [(MOI.ScalarAffineFunction{Int},MOI.Interval{Int})] - test_noc(model, MOI.ScalarAffineFunction{Int}, MOI.GreaterThan{Int}, 0) - test_noc(model, MOI.ScalarAffineFunction{Int}, MOI.Interval{Int}, 1) + test_num_constraints(model, MOI.ScalarAffineFunction{Int}, MOI.GreaterThan{Int}, 0) + test_num_constraints(model, MOI.ScalarAffineFunction{Int}, MOI.Interval{Int}, 1) @test (@inferred MOI.get(model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Int},MOI.Interval{Int}}())) == [c1] end diff --git a/test/Bridges/utilities.jl b/test/Bridges/utilities.jl index a1ea48ac8a..861ce3a615 100644 --- a/test/Bridges/utilities.jl +++ b/test/Bridges/utilities.jl @@ -1,36 +1,93 @@ -function test_noc(bridged_mock, F, S, n) +function test_num_constraints(bridged_mock, F, S, n) @test MOI.get(bridged_mock, MOI.NumberOfConstraints{F, S}()) == n @test length(MOI.get(bridged_mock, MOI.ListOfConstraintIndices{F, S}())) == n @test ((F, S) in MOI.get(bridged_mock, MOI.ListOfConstraints())) == !iszero(n) end -# Test deletion of bridge +# Test deletion of constraint bridge used for constraint `ci` function test_delete_bridge( m::MOIB.AbstractBridgeOptimizer, ci::MOI.ConstraintIndex{F, S}, nvars::Int, - nocs::Tuple; used_bridges = 1, num_bridged = 1) where {F, S} + list_num_constraints::Tuple; used_bridges = 1, num_bridged = 1) where {F, S} function num_bridges() return count(bridge -> true, values(MOIB.Constraint.bridges(m))) end start_num_bridges = num_bridges() @test MOI.get(m, MOI.NumberOfVariables()) == nvars - test_noc(m, F, S, num_bridged) - for noc in nocs - test_noc(m, noc...) + test_num_constraints(m, F, S, num_bridged) + for num_constraints in list_num_constraints + test_num_constraints(m, num_constraints...) end @test MOI.is_valid(m, ci) MOI.delete(m, ci) - @test_throws MOI.InvalidIndex{typeof(ci)} MOI.delete(m, ci) - try - MOI.delete(m, ci) - catch err - @test err.index == ci - end + @test_throws MOI.InvalidIndex(ci) MOI.delete(m, ci) @test !MOI.is_valid(m, ci) @test num_bridges() == start_num_bridges - used_bridges - test_noc(m, F, S, num_bridged - 1) + test_num_constraints(m, F, S, num_bridged - 1) # As the bridge has been removed, if the constraints it has created where not removed, it wouldn't be there to decrease this counter anymore @test MOI.get(m, MOI.NumberOfVariables()) == nvars - for noc in nocs - test_noc(m, noc...) + for num_constraints in list_num_constraints + test_num_constraints(m, num_constraints...) + end +end +# Test deletion of variable bridge used for variable `vi` +function test_delete_bridged_variable( + m::MOIB.AbstractBridgeOptimizer, vi::MOI.VariableIndex, S::Type, + nvars::Int, list_num_constraints::Tuple; used_bridges = 1, num_bridged = 1, used_constraints = 1) + function num_bridges() + return count(bridge -> true, values(MOIB.Variable.bridges(m))) + end + start_num_bridges = num_bridges() + @test MOI.get(m, MOI.NumberOfVariables()) == nvars + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars + if S != MOI.Reals + F = S <: MOI.AbstractScalarSet ? MOI.SingleVariable : MOI.VectorOfVariables + test_num_constraints(m, F, S, num_bridged) + end + for num_constraints in list_num_constraints + test_num_constraints(m, num_constraints...) + end + @test MOI.is_valid(m, vi) + MOI.delete(m, vi) + @test_throws MOI.InvalidIndex(vi) MOI.delete(m, vi) + @test !MOI.is_valid(m, vi) + @test num_bridges() == start_num_bridges - used_bridges + if S != MOI.Reals + test_num_constraints(m, F, S, num_bridged - used_constraints) + end + @test MOI.get(m, MOI.NumberOfVariables()) == nvars - 1 + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars - 1 + for num_constraints in list_num_constraints + test_num_constraints(m, num_constraints...) + end +end +# Test deletion of variable bridge used for vector of variables `vis` +function test_delete_bridged_variables( + m::MOIB.AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}, S::Type, + nvars::Int, list_num_constraints::Tuple; used_bridges = 1, num_bridged = 1) + function num_bridges() + return count(bridge -> true, values(MOIB.Variable.bridges(m))) + end + start_num_bridges = num_bridges() + @test MOI.get(m, MOI.NumberOfVariables()) == nvars + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars + if S != MOI.Reals + F = S <: MOI.AbstractScalarSet ? MOI.SingleVariable : MOI.VectorOfVariables + test_num_constraints(m, F, S, num_bridged) + end + for num_constraints in list_num_constraints + test_num_constraints(m, num_constraints...) + end + @test all(vi -> MOI.is_valid(m, vi), vis) + MOI.delete(m, vis) + @test_throws MOI.InvalidIndex(vis[1]) MOI.delete(m, vis) + @test all(vi -> !MOI.is_valid(m, vi), vis) + @test num_bridges() == start_num_bridges - used_bridges + if S != MOI.Reals + test_num_constraints(m, F, S, num_bridged - 1) + end + @test MOI.get(m, MOI.NumberOfVariables()) == nvars - length(vis) + @test length(MOI.get(m, MOI.ListOfVariableIndices())) == nvars - length(vis) + for num_constraints in list_num_constraints + test_num_constraints(m, num_constraints...) end end