Unexpected JuMP variable order when indexed by tuple

When creating a variable indexed by Tuple{Int64, Float64}, I get a different order (apparently, sorted as string) than that provided when creating or accessing the variable.

Steps to reproduce:

using JuMP

model = Model()

N = 1:2
M = [[1.0, 11.0, 100.0], [0.0, 17.0, 104.0]]

@variable(model, foo[n in N, M[n]])

foo[1, M[1]]

Current outcome:

JuMP.Containers.SparseAxisArray{VariableRef, 2, Tuple{Int64, Float64}} with 6 entries:
  [1, 1.0  ]  =  foo[1,1.0]
  [1, 100.0]  =  foo[1,100.0]
  [1, 11.0 ]  =  foo[1,11.0]
  [2, 0.0  ]  =  foo[2,0.0]
  [2, 104.0]  =  foo[2,104.0]
  [2, 17.0 ]  =  foo[2,17.0]

Expected outcome:

JuMP.Containers.SparseAxisArray{VariableRef, 2, Tuple{Int64, Float64}} with 6 entries:
  [1, 1.0  ]  =  foo[1,1.0]
  [1, 11.0 ]  =  foo[1,11.0]
  [1, 100.0]  =  foo[1,100.0]
  [2, 0.0  ]  =  foo[2,0.0]
  [2, 17.0 ]  =  foo[2,17.0]
  [2, 104.0]  =  foo[2,104.0]

I’m having a huge problem with SOS2 constraints, as something like

@constraint(model, C[n in N], foo[n, M[n]] in SOS2(M[n]))

enforces the constraint on a different order than that established by M, resulting in

 C[1] : [foo[1,11.0], foo[1,1.0], foo[1,100.0]] ∈ MathOptInterface.SOS2{Float64}([1.0, 11.0, 100.0])
 C[2] : [foo[2,0.0], foo[2,104.0], foo[2,17.0]] ∈ MathOptInterface.SOS2{Float64}([0.0, 17.0, 104.0])

Is that something expected? It seems very counter-intuitive for me.

EDIT: I’m using Julia 1.9.3 and JuMP v1.19.0.
EDIT2: fix constraint, extend example wrt the SOS2 constraint.

Maybe you can use OrderedDict as the container for the variable?

Containers · JuMP is the link, I think (on the phone).

Using a different container seems a good workaround, but OrderedDicts are not sliceable.

My current workaround for the SOS2 problem is

@constraint(model, C[n in N], [foo[n, m] for m in M[n]] in SOS2(M[n]))
1 Like

First, hi @brunompacheco, welcome to the forum :smile:

This isn’t valid JuMP syntax?

But anyway, your suggested workaround is the recommended syntax.

A SparseAxisArray is more a wrapper around a Dict than an Array. A better name might have been SliceableNDimensionalDict.

julia> foo
JuMP.Containers.SparseAxisArray{VariableRef, 2, Tuple{Int64, Float64}} with 4 entries:
  [1, 100.0]  =  foo[1,100.0]
  [1, 11.0 ]  =  foo[1,11.0]
  [2, 100.0]  =  foo[2,100.0]
  [2, 11.0 ]  =  foo[2,11.0]

julia> foo.data
Dict{Tuple{Int64, Float64}, VariableRef} with 4 entries:
  (1, 11.0)  => foo[1,11.0]
  (1, 100.0) => foo[1,100.0]
  (2, 11.0)  => foo[2,11.0]
  (2, 100.0) => foo[2,100.0]

This is a useful reminder that you do not need to be restricted to the build-in JuMP types: Variables · JuMP

Sometimes (often) other data structures will be more useful and appropriate.

Thanks! I have just fixed the constraint expression and extended the example, just in case others face the same problem.

I still find it quite counter-intuitive that my workaround is necessary, as the example in JuMP docs made it seem to me that the variable’s index order would be preserved.

Do you suggest other container structure that would be more appropriate in this case?

it seem to me that the variable’s index order would be preserved.

In that case, the x is a Vector. Perhaps we could throw an error (or a warning) if we try to convert a SparseAxisArray into a Vector as part of the constraints.

Do you suggest other container structure that would be more appropriate in this case?

The SparseAxisArray doesn’t really model what you need. Would you ever slice in the other dimension foo[:, 11.0]?

You really have a vector of vectors:

using JuMP
model = Model()
N = 1:2
M = [[1.0, 11.0, 100.0], [0.0, 17.0, 104.0]]
foo = [@variable(model, [M[n]], base_name = "foo[$n]") for n in 1:2]
@constraint(model, [n in N], foo[n] in SOS2(M[n]))

p.s. I opened a bug report: Slicing SparseAxisArray does not preserve order · Issue #3678 · jump-dev/JuMP.jl · GitHub