diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index 0f41b3860c..a3a68aa6ab 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -17,6 +17,8 @@ include("single_bridge_optimizer.jl") # Variable bridges include("zeros.jl") const Zeros{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{ZerosBridge{T}, OT} +include("free.jl") +const Free{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{FreeBridge{T}, OT} include("flip_sign.jl") const NonposToNonneg{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{NonposToNonnegBridge{T}, OT} include("vectorize.jl") diff --git a/src/Bridges/Variable/free.jl b/src/Bridges/Variable/free.jl new file mode 100644 index 0000000000..7dfaef304f --- /dev/null +++ b/src/Bridges/Variable/free.jl @@ -0,0 +1,117 @@ +""" + FreeBridge{T} <: Bridges.Variable.AbstractBridge + +Transforms constrained variables in [`MOI.Reals`](@ref) to the difference of +constrained variables in [`MOI.Nonnegatives`](@ref). +""" +struct FreeBridge{T} <: AbstractBridge + variables::Vector{MOI.VariableIndex} + constraint::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Nonnegatives} +end +function bridge_constrained_variable(::Type{FreeBridge{T}}, + model::MOI.ModelLike, + set::MOI.Reals) where T + variables, constraint = MOI.add_constrained_variables( + model, MOI.Nonnegatives(2MOI.dimension(set))) + return FreeBridge{T}(variables, constraint) +end + +function supports_constrained_variable( + ::Type{<:FreeBridge}, ::Type{MOI.Reals}) + return true +end +function MOIB.added_constrained_variable_types(::Type{<:FreeBridge}) + return [(MOI.Nonnegatives,)] +end +function MOIB.added_constraint_types(::Type{FreeBridge{T}}) where T + return Tuple{DataType, DataType}[] +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::FreeBridge, ::MOI.NumberOfVariables) + return length(bridge.variables) +end +function MOI.get(bridge::FreeBridge, ::MOI.ListOfVariableIndices) + return vcat(bridge.variables) +end +function MOI.get(bridge::FreeBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, + MOI.Nonnegatives}) + return 1 +end +function MOI.get(bridge::FreeBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, + MOI.Nonnegatives}) + return [bridge.constraint] +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::FreeBridge) + MOI.delete(model, bridge.variables) +end + +function MOI.delete(model::MOI.ModelLike, bridge::FreeBridge, i::IndexInVector) + n = div(length(bridge.variables), 2) + MOI.delete(model, bridge.variables[i.value]) + MOI.delete(model, bridge.variables[n + i.value]) + deleteat!(bridge.variables, [i.value, n + i.value]) +end + + +# Attributes, Bridge acting as a constraint + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::FreeBridge{T}) where T + n = div(length(bridge.variables), 2) + primal = MOI.get(model, attr, bridge.constraint) + return primal[1:n] - primal[n .+ (1:n)] +end +# The transformation is x_free = [I -I] * x +# so the transformation of the dual is +# y = [I; -I] * y_free +# that is +# y[1:n] = -y[n .+ (1:n)] = y_free +# We can therefore compute `y_free` from either of them, let's take `y[1:n]`. +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::FreeBridge{T}) where T + n = div(length(bridge.variables), 2) + return MOI.get(model, attr, bridge.constraint)[1:n] +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::FreeBridge{T}, i::IndexInVector) where T + n = div(length(bridge.variables), 2) + return MOI.get(model, attr, bridge.variables[i.value]) - + MOI.get(model, attr, bridge.variables[n + i.value]) +end + +function MOIB.bridged_function(bridge::FreeBridge{T}, i::IndexInVector) where T + n = div(length(bridge.variables), 2) + return MOIU.operate(-, T, MOI.SingleVariable(bridge.variables[i.value]), + MOI.SingleVariable(bridge.variables[n + i.value])) +end +# x_free has been replaced by x[i] - x[n + i]. +# To undo it we replace x[i] by x_free and x[n + i] by 0. +function unbridged_map(bridge::FreeBridge{T}, vi::MOI.VariableIndex, + i::IndexInVector) where T + sv = MOI.SingleVariable(vi) + # `unbridged_map` is required to return a `MOI.ScalarAffineFunction`. + func = convert(MOI.ScalarAffineFunction{T}, sv) + n = div(length(bridge.variables), 2) + return bridge.variables[i.value] => func, + bridge.variables[n + i.value] => zero(MOI.ScalarAffineFunction{T}) +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart, + bridge::FreeBridge, value, i::IndexInVector) + if value < 0 + nonneg = zero(value) + nonpos = -value + else + nonneg = value + nonpos = zero(value) + end + n = div(length(bridge.variables), 2) + MOI.set(model, attr, bridge.variables[i.value], nonneg) + MOI.set(model, attr, bridge.variables[n + i.value], nonpos) +end diff --git a/test/Bridges/Variable/Variable.jl b/test/Bridges/Variable/Variable.jl index e6520c5c23..c8c601430c 100644 --- a/test/Bridges/Variable/Variable.jl +++ b/test/Bridges/Variable/Variable.jl @@ -5,6 +5,9 @@ end @testset "Zeros" begin include("zeros.jl") end +@testset "Free" begin + include("free.jl") +end @testset "FlipSign" begin include("flip_sign.jl") end diff --git a/test/Bridges/Variable/free.jl b/test/Bridges/Variable/free.jl new file mode 100644 index 0000000000..d1c1212723 --- /dev/null +++ b/test/Bridges/Variable/free.jl @@ -0,0 +1,187 @@ +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.UniversalFallback(MOIU.Model{Float64}())) +config = MOIT.TestConfig() + +bridged_mock = MOIB.Variable.Free{Float64}(mock) + +@testset "solve_multirow_vectoraffine_nonpos" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [0.5, 0.0]) + ), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, + MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [0.25, 0.0]) + ) + ) + MOIT.solve_multirow_vectoraffine_nonpos(bridged_mock, config) + + MOI.set(bridged_mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.VectorAffineFunction{Float64}, MOI.Nonpositives}()), + ["c"]) + + @testset "Mock model" begin + var_names = ["xpos", "xneg"] + MOI.set(mock, MOI.VariableName(), + MOI.get(mock, MOI.ListOfVariableIndices()), var_names) + MOI.set(bridged_mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonnegatives}()), + ["nonneg"]) + s = """ + variables: xpos, xneg + nonneg: [xpos, xneg] in MathOptInterface.Nonnegatives(2) + c: [4.0xpos + -4.0xneg + -1.0, 3.0xpos + -3.0xneg + -1.0] in MathOptInterface.Nonpositives(2) + maxobjective: xpos + -1.0xneg + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(mock, model, var_names, ["nonneg", "c"]) + end + @testset "Bridged model" begin + s = """ + variables: x + c: [4.0x + -1.0, 3.0x + -1.0] in MathOptInterface.Nonpositives(2) + maxobjective: 1.0x + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(bridged_mock, model, ["x"], ["c"]) + end +end + +@testset "Linear6" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, 100], + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) => [1.0], + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => [-1.0] + )) + MOIT.linear6test(bridged_mock, config) + + loc = MOI.get(bridged_mock, MOI.ListOfConstraints()) + @test length(loc) == 2 + @test !((MOI.VectorOfVariables, MOI.Reals) in loc) + @test (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) in loc + @test (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) in loc + @test MOI.get(mock, MOI.NumberOfVariables()) == 4 + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 2 + vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test vis == MOI.VariableIndex.([-1, -2]) + + cx = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(vis[1].value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cx) == [100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cx) == [0.0] + cy = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(vis[2].value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cy) == [-100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cy) == [0.0] + + test_delete_bridged_variable(bridged_mock, vis[1], MOI.Reals, 2, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + )) + test_delete_bridged_variable(bridged_mock, vis[2], MOI.Reals, 1, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + )) +end + +@testset "Linear7" begin + function set_mock_optimize_linear7Test!(mock) + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0, 0, 100], + (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[1.0]], + (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1.0]] + )) + end + set_mock_optimize_linear7Test!(mock) + MOIT.linear7test(bridged_mock, config) + + x, y = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + + cx = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(x.value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cx) == [100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cx) == [0.0] + cy = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.Reals}(y.value) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cy) == [-100.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), cy) == [0.0] + + @test MOI.supports(bridged_mock, MOI.VariablePrimalStart(), MOI.VariableIndex) + MOI.set(bridged_mock, MOI.VariablePrimalStart(), [x, y], [1.0, -1.0]) + xa, xb, ya, yb = MOI.get(mock, MOI.ListOfVariableIndices()) + @test MOI.get(mock, MOI.VariablePrimalStart(), [xa, xb, ya, yb]) == [1.0, 0.0, 0.0, 1.0] +end + +@testset "Linear11" begin + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1.0, 0.0, 0.0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0.5, 0.5, 0.0, 0.0])) + MOIT.linear11test(bridged_mock, config) + + vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test vis == MOI.VariableIndex.([-1, -2]) + + MOI.set(bridged_mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}()), + ["c1"]) + MOI.set(bridged_mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}}()), + ["c2"]) + + @testset "Mock model" begin + var_names = ["v1pos", "v2pos", "v1neg", "v2neg"] + MOI.set(mock, MOI.VariableName(), + MOI.get(mock, MOI.ListOfVariableIndices()), var_names) + MOI.set(bridged_mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonnegatives}()), + ["nonneg"]) + s = """ + variables: v1pos, v2pos, v1neg, v2neg + nonneg: [v1pos, v2pos, v1neg, v2neg] in MathOptInterface.Nonnegatives(4) + c1: v1pos + -1.0v1neg + v2pos + -1.0v2neg >= 1.0 + c2: v1pos + -1.0v1neg + v2pos + -1.0v2neg <= 2.0 + minobjective: v1pos + -1.0v1neg + v2pos + -1.0v2neg + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(mock, model, var_names, ["nonneg", "c1", "c2"]) + end + @testset "Bridged model" begin + var_names = ["v1", "v2"] + MOI.set(bridged_mock, MOI.VariableName(), + vis, var_names) + s = """ + variables: v1, v2 + c1: v1 + v2 >= 1.0 + c2: v1 + v2 <= 2.0 + minobjective: v1 + v2 + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(bridged_mock, model, var_names, ["c1", "c2"]) + end + + test_delete_bridged_variable(bridged_mock, vis[1], MOI.Reals, 2, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + ), used_bridges = 0, used_constraints = 0) + test_delete_bridged_variable(bridged_mock, vis[2], MOI.Reals, 1, ( + (MOI.VectorOfVariables, MOI.Nonnegatives, 0), + (MOI.VectorOfVariables, MOI.Nonpositives, 0) + )) +end