Elegant way to do multi-index algebra

I want to use a number to represent a multi-index. Consider i in 1:3 and j in 1:3. I want 3 to represent for (0,3), 4 for (1,1), 8 for (3,2) and so forth.

This can be done by i = (num-1)÷3 + 1 and j = (num-1)%3 + 1. This seems not quite elegant. Is there a better way to do this job? This is the first time I think 0-based indexing is superior…

Julia already supports linear indexing of multidimensional arrays.

Note that the i = (num-1)÷3 + 1 and j = (num-1)%3 + 1 formula you gave is row-major but Julia is column-major. If you really care about this you can just reverse the index order or transpose the array.

julia> A = [(i,j) for i=1:3, j=1:3]
3×3 Matrix{Tuple{Int64, Int64}}:
 (1, 1)  (1, 2)  (1, 3)
 (2, 1)  (2, 2)  (2, 3)
 (3, 1)  (3, 2)  (3, 3)

julia> A[3], A[4], A[8]
((3, 1), (1, 2), (2, 3))

You can get both the (i,j) -> n and n -> (i,j) maps without allocation (as array-like objects that compute elements on the fly) using LinearIndices and CartesianIndices, respectively:

julia> C = CartesianIndices((3, 3)) # indices of a 3x3 array
CartesianIndices((3, 3))

julia> collect(C) # collect and display all the elements explicitly
3×3 Matrix{CartesianIndex{2}}:
 CartesianIndex(1, 1)  CartesianIndex(1, 2)  CartesianIndex(1, 3)
 CartesianIndex(2, 1)  CartesianIndex(2, 2)  CartesianIndex(2, 3)
 CartesianIndex(3, 1)  CartesianIndex(3, 2)  CartesianIndex(3, 3)

julia> C[3], C[4], C[8]
(CartesianIndex(3, 1), CartesianIndex(1, 2), CartesianIndex(2, 3))

julia> L = LinearIndices(C) # alternatively: L = LinearIndices((3, 3))
3×3 LinearIndices{2, Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}}}:
 1  4  7
 2  5  8
 3  6  9

julia> L[3,1], L[1,2], L[2,3]
(3, 4, 8)
3 Likes

Thanks for your answer. I know julia has linear indexing for multidimensional arrays, but I’m working on a custom type.

The trick using an index matrix is interesting, but the size of my type is not fixed. That means I need to generate a new matrix when the sized is changed. I will think about how to deal with this.

Thank you!

CartesianIndices and LinearIndices aren’t “index matrices” and they don’t need to be “generated” — creating them is essentially free, because they really just store the size of the array:

julia> dump(L)
LinearIndices{2, Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}}}
  indices: Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}}
    1: Base.OneTo{Int64}
      stop: Int64 3
    2: Base.OneTo{Int64}
      stop: Int64 3

julia> dump(C)
CartesianIndices{2, Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}}}
  indices: Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}}
    1: Base.OneTo{Int64}
      stop: Int64 3
    2: Base.OneTo{Int64}
      stop: Int64 3

and the indices are calculated only when you do L[i,j] or C[i]. Internally, they are doing calculations much like the one you suggested.

They are array-like objects, similar to ranges 1:n, that act like arrays but don’t store their elements.

8 Likes

Thank you for the explanation!