Skip to content

Commit 8f2f253

Browse files
authored
[Bridges] fix ordering of variables returned by LazyBridgeOptimizer (#2695)
1 parent 6bfca94 commit 8f2f253

File tree

5 files changed

+201
-13
lines changed

5 files changed

+201
-13
lines changed

src/Bridges/Variable/map.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,8 @@ Base.keys(::EmptyMap) = MOI.Utilities.EmptyVector{MOI.VariableIndex}()
691691

692692
Base.values(::EmptyMap) = MOI.Utilities.EmptyVector{AbstractBridge}()
693693

694+
Base.iterate(::EmptyMap) = nothing
695+
694696
has_bridges(::EmptyMap) = false
695697

696698
number_of_variables(::EmptyMap) = 0

src/Bridges/bridge_optimizer.jl

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -717,12 +717,76 @@ function _get_all_including_bridged(
717717
b::AbstractBridgeOptimizer,
718718
attr::MOI.ListOfVariableIndices,
719719
)
720-
list = MOI.get(b.model, attr)
721-
if !isempty(Variable.bridges(b))
722-
# The conversion from `keys` into a `Vector` happens inside `append!`.
723-
append!(list, keys(Variable.bridges(b)))
720+
# `inner_to_outer` is going to map variable indices in `b.model` to their
721+
# bridged variable indices. If the bridge adds multiple variables, we need
722+
# only to map the first variable, and we can skip the rest. To mark this
723+
# distinction, the tail variables are set to `nothing`.
724+
map = Variable.bridges(b)
725+
inner_to_outer = Dict{MOI.VariableIndex,Union{Nothing,MOI.VariableIndex}}()
726+
# These are variables which appear in `b` but do NOT appear in `b.model`.
727+
# One reason might be the Zero bridge in which they are replaced by `0.0`.
728+
user_only_variables = MOI.VariableIndex[]
729+
for (user_variable, bridge) in map
730+
variables = MOI.get(bridge, MOI.ListOfVariableIndices())
731+
if isempty(variables)
732+
# This bridge maps `user_variable` to constants in `b.model`. We
733+
# still need to report back `user_variable` to the user. In
734+
# addition, it might represent the start of a VectorOfVariables
735+
# function, so we may need to report multiple variables.
736+
push!(user_only_variables, user_variable)
737+
n = Variable.length_of_vector_of_variables(map, user_variable)
738+
for i in 1:n-1
739+
push!(
740+
user_only_variables,
741+
MOI.VariableIndex(user_variable.value - i),
742+
)
743+
end
744+
else
745+
# This bridge maps `user_variable` to a list of `variables`. We need
746+
# to swap out only the `first` variable. The others can be flagged
747+
# with `nothing`. To simplify the first/tail loop, we set
748+
# `first(variables)` twice; first to `nothing` and then to
749+
# `user_variable`.
750+
for bridged_variable in variables
751+
inner_to_outer[bridged_variable] = nothing
752+
end
753+
inner_to_outer[first(variables)] = user_variable
754+
end
724755
end
725-
return list
756+
# We're about to loop over the variables in `.model`, ordered by when they
757+
# were added to the model. We need to return a list of the original
758+
# user-space variables in the order that they were added. To do so, we need
759+
# to undo any Variable.bridges transformations.
760+
ret = MOI.VariableIndex[]
761+
for inner_variable in MOI.get(b.model, attr)
762+
outer_variable = get(inner_to_outer, inner_variable, missing)
763+
if ismissing(outer_variable)
764+
# inner_variable does not exist in inner_to_outer, which means that
765+
# it is not bridged. Pass through unchanged.
766+
push!(ret, inner_variable)
767+
elseif isnothing(outer_variable)
768+
# inner_variable exists in inner_to_outer, but it is set to `nothing`
769+
# which means that it is not the first variable in the bridge. Skip
770+
# it because it should be hidden from the user.
771+
else
772+
# inner_variable exists in inner_to_outer. It must be the first
773+
# variable in the bridge. Report it back to the user.
774+
push!(ret, outer_variable)
775+
# `outer_variable` might represent the start of a VectorOfVariables
776+
# if multiple user-variables were bridged. Add them all.
777+
n = Variable.length_of_vector_of_variables(map, outer_variable)
778+
for i in 1:n-1
779+
push!(ret, MOI.VariableIndex(outer_variable.value - i))
780+
end
781+
end
782+
end
783+
# Since these were replaced by constants, we don't actually know when they
784+
# were added to the model. Tack them on at the end...
785+
#
786+
# If you, future reader, find this troublesome, come up with a better
787+
# solution.
788+
append!(ret, user_only_variables)
789+
return ret
726790
end
727791

728792
function _get_all_including_bridged(

src/Test/test_basic_constraint.jl

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ function _basic_constraint_test_helper(
219219
set = _set(T, UntypedS)
220220
N = MOI.dimension(set)
221221
x = add_variables_fn(model, N)
222-
223222
constraint_function = _function(T, UntypedF, x)
224223
@assert MOI.output_dimension(constraint_function) == N
225224
F, S = typeof(constraint_function), typeof(set)
@@ -235,9 +234,21 @@ function _basic_constraint_test_helper(
235234
@test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1
236235
_test_attribute_value_type(model, MOI.NumberOfConstraints{F,S}())
237236
###
237+
### Test MOI.ListOfConstraintIndices
238+
###
239+
c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
240+
@test c_indices == [c]
241+
###
242+
### Test MOI.ListOfConstraintTypesPresent
243+
###
244+
@test (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent())
245+
###
238246
### Test MOI.is_valid
239247
###
240248
@test MOI.is_valid(model, c)
249+
# TODO(odow): we could improve this test by checking `c.value+1` and
250+
# `c.value-1` but there is a bug in `LazyBridgeOptimizer`.
251+
@test !MOI.is_valid(model, typeof(c)(c.value + 12345))
241252
###
242253
### Test MOI.ConstraintName
243254
###
@@ -287,11 +298,6 @@ function _basic_constraint_test_helper(
287298
_test_attribute_value_type(model, MOI.ConstraintSet(), c)
288299
end
289300
###
290-
### Test MOI.ListOfConstraintIndices
291-
###
292-
c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
293-
@test length(c_indices) == 1
294-
###
295301
### Test MOI.add_constraints
296302
###
297303
if F != MOI.VariableIndex && F != MOI.VectorOfVariables

src/Test/test_conic.jl

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7219,3 +7219,117 @@ function setup_test(
72197219
end
72207220

72217221
version_added(::typeof(test_conic_NormCone)) = v"1.14.0"
7222+
7223+
function test_add_constrained_PositiveSemidefiniteConeTriangle(
7224+
model::MOI.ModelLike,
7225+
config::Config{T},
7226+
) where {T<:Real}
7227+
@requires MOI.supports_add_constrained_variables(
7228+
model,
7229+
MOI.PositiveSemidefiniteConeTriangle,
7230+
)
7231+
@requires _supports(config, MOI.delete)
7232+
# Add a scalar
7233+
x = MOI.add_variable(model)
7234+
@test MOI.get(model, MOI.ListOfVariableIndices()) == [x]
7235+
@test MOI.is_valid(model, x)
7236+
@test MOI.get(model, MOI.NumberOfVariables()) == 1
7237+
# Add a PSD matrix
7238+
set = MOI.PositiveSemidefiniteConeTriangle(2)
7239+
X, c = MOI.add_constrained_variables(model, set)
7240+
F, S = MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle
7241+
@test (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent())
7242+
@test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c]
7243+
@test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1
7244+
@test MOI.is_valid(model, c)
7245+
@test !MOI.is_valid(model, typeof(c)(c.value - 1))
7246+
@test !MOI.is_valid(model, typeof(c)(c.value + 1))
7247+
@test MOI.get(model, MOI.ListOfVariableIndices()) == [x; X]
7248+
@test all(MOI.is_valid.(model, [x; X]))
7249+
@test MOI.get(model, MOI.NumberOfVariables()) == 4
7250+
# Add and delete a scalar
7251+
y = MOI.add_variable(model)
7252+
@test MOI.get(model, MOI.ListOfVariableIndices()) == [x; X; y]
7253+
@test all(MOI.is_valid.(model, [x; X; y]))
7254+
@test MOI.get(model, MOI.NumberOfVariables()) == 5
7255+
MOI.delete(model, y)
7256+
@test !MOI.is_valid(model, y)
7257+
@test MOI.get(model, MOI.ListOfVariableIndices()) == [x; X]
7258+
@test all(MOI.is_valid.(model, [x; X]))
7259+
@test MOI.get(model, MOI.NumberOfVariables()) == 4
7260+
# Add and delete another scalar
7261+
z = MOI.add_variable(model)
7262+
@test MOI.get(model, MOI.ListOfVariableIndices()) == [x; X; z]
7263+
@test all(MOI.is_valid.(model, [x; X]))
7264+
@test MOI.is_valid(model, z)
7265+
@test MOI.get(model, MOI.NumberOfVariables()) == 5
7266+
MOI.delete(model, z)
7267+
@test !MOI.is_valid(model, y)
7268+
@test !MOI.is_valid(model, z)
7269+
@test MOI.get(model, MOI.ListOfVariableIndices()) == [x; X]
7270+
@test all(MOI.is_valid.(model, [x; X]))
7271+
@test MOI.get(model, MOI.NumberOfVariables()) == 4
7272+
MOI.delete(model, x)
7273+
@test !MOI.is_valid(model, x)
7274+
@test !MOI.is_valid(model, y)
7275+
@test !MOI.is_valid(model, z)
7276+
@test MOI.get(model, MOI.ListOfVariableIndices()) == X
7277+
@test all(MOI.is_valid.(model, X))
7278+
@test MOI.get(model, MOI.NumberOfVariables()) == 3
7279+
return
7280+
end
7281+
7282+
function version_added(
7283+
::typeof(test_add_constrained_PositiveSemidefiniteConeTriangle),
7284+
)
7285+
return v"1.38.1"
7286+
end
7287+
7288+
function test_add_constrained_PositiveSemidefiniteConeTriangle_VariableName(
7289+
model::MOI.ModelLike,
7290+
config::Config{T},
7291+
) where {T<:Real}
7292+
@requires MOI.supports_add_constrained_variables(
7293+
model,
7294+
MOI.PositiveSemidefiniteConeTriangle,
7295+
)
7296+
@requires MOI.supports(model, MOI.VariableName(), MOI.VariableIndex)
7297+
set = MOI.PositiveSemidefiniteConeTriangle(2)
7298+
X, _ = MOI.add_constrained_variables(model, set)
7299+
MOI.set(model, MOI.VariableName(), X[1], "x")
7300+
@test MOI.get(model, MOI.VariableName(), X[1]) == "x"
7301+
return
7302+
end
7303+
7304+
function version_added(
7305+
::typeof(
7306+
test_add_constrained_PositiveSemidefiniteConeTriangle_VariableName,
7307+
),
7308+
)
7309+
return v"1.38.1"
7310+
end
7311+
7312+
function test_add_constrained_PositiveSemidefiniteConeTriangle_VariablePrimalStart(
7313+
model::MOI.ModelLike,
7314+
config::Config{T},
7315+
) where {T<:Real}
7316+
@requires MOI.supports_add_constrained_variables(
7317+
model,
7318+
MOI.PositiveSemidefiniteConeTriangle,
7319+
)
7320+
@requires MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)
7321+
set = MOI.PositiveSemidefiniteConeTriangle(2)
7322+
X, _ = MOI.add_constrained_variables(model, set)
7323+
@test all(isnothing, MOI.get.(model, MOI.VariablePrimalStart(), X))
7324+
MOI.set.(model, MOI.VariablePrimalStart(), X, [1.0, 0.0, 1.0])
7325+
@test MOI.get.(model, MOI.VariablePrimalStart(), X) == [1.0, 0.0, 1.0]
7326+
return
7327+
end
7328+
7329+
function version_added(
7330+
::typeof(
7331+
test_add_constrained_PositiveSemidefiniteConeTriangle_VariablePrimalStart,
7332+
),
7333+
)
7334+
return v"1.38.1"
7335+
end

test/Bridges/Variable/NonposToNonnegBridge.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ function test_NonposToNonneg()
4040
)
4141
@test MOI.get(mock, MOI.NumberOfVariables()) == 4
4242
@test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 4
43+
# Variables are ordered
44+
# x in R^1, y in R_-^1, z in R^1, s in R^1
4345
vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices())
44-
y = vis[4]
46+
y = vis[2]
4547
@test y.value == -1
4648

4749
@test MOI.supports(
@@ -90,7 +92,7 @@ function test_NonposToNonneg()
9092
MOI.set(
9193
bridged_mock,
9294
MOI.VariableName(),
93-
MOI.get(bridged_mock, MOI.ListOfVariableIndices())[4],
95+
MOI.get(bridged_mock, MOI.ListOfVariableIndices())[2],
9496
"v",
9597
)
9698
con_v = MOI.get(

0 commit comments

Comments
 (0)