Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Containers do not support view #2998

Closed
odow opened this issue Jun 7, 2022 · 17 comments
Closed

Containers do not support view #2998

odow opened this issue Jun 7, 2022 · 17 comments
Labels
Category: Containers Related to the Containers submodule Type: Bug
Milestone

Comments

@odow
Copy link
Member

odow commented Jun 7, 2022

From Pierre Pasquet on slack:

julia> x = Containers.@container([i=[:a, :b]], 0)
1-dimensional DenseAxisArray{Int64,1,...} with index sets:
    Dimension 1, [:a, :b]
And data, a 2-element Vector{Int64}:
 0
 0

julia> view(x, :)
ERROR: MethodError: no method matching Base.Slice(::Vector{Symbol})
Closest candidates are:
  Base.Slice(::Base.Slice) at indices.jl:353
  Base.Slice(::T) where T<:AbstractUnitRange at indices.jl:351
Stacktrace:
 [1] uncolon(inds::Tuple{Vector{Symbol}}, I::Tuple{Colon})
   @ Base ./multidimensional.jl:827
 [2] to_indices
   @ ./multidimensional.jl:822 [inlined]
 [3] to_indices
   @ ./indices.jl:325 [inlined]
 [4] view(A::JuMP.Containers.DenseAxisArray{Int64, 1, Tuple{Vector{Symbol}}, Tuple{JuMP.Containers._AxisLookup{Dict{Symbol, Int64}}}}, I::Function)
   @ Base ./subarray.jl:176
 [5] top-level scope
   @ REPL[7]:1

We should throw a nicer error message, since this might be something that people do more often in future.

@odow odow added the Category: Containers Related to the Containers submodule label Jun 7, 2022
@odow
Copy link
Member Author

odow commented Jun 7, 2022

Annoyingly, it works if the indices are AbstractUnitRange:

julia> x = Containers.@container([i=1:2], 0)
2-element Vector{Int64}:
 0
 0

julia> view(x, :)
2-element view(::Vector{Int64}, :) with eltype Int64:
 0
 0

julia> x = Containers.@container([i=2:3], 0)
1-dimensional DenseAxisArray{Int64,1,...} with index sets:
    Dimension 1, 2:3
And data, a 2-element Vector{Int64}:
 0
 0

julia> v = view(x, :)
2-element view(::JuMP.Containers.DenseAxisArray{Int64, 1, Tuple{UnitRange{Int64}}, Tuple{JuMP.Containers._AxisLookup{Tuple{Int64, Int64}}}}, :) with eltype Int64 with indices 2:3:
 0
 0

julia> v[1] = 1
ERROR: BoundsError: attempt to access 2-element view(::JuMP.Containers.DenseAxisArray{Int64, 1, Tuple{UnitRange{Int64}}, Tuple{JuMP.Containers._AxisLookup{Tuple{Int64, Int64}}}}, :) with eltype Int64 with indices 2:3 at index [1]
Stacktrace:
 [1] throw_boundserror(A::SubArray{Int64, 1, JuMP.Containers.DenseAxisArray{Int64, 1, Tuple{UnitRange{Int64}}, Tuple{JuMP.Containers._AxisLookup{Tuple{Int64, Int64}}}}, Tuple{Base.Slice{UnitRange{Int64}}}, false}, I::Tuple{Int64})
   @ Base ./abstractarray.jl:651
 [2] checkbounds
   @ ./abstractarray.jl:616 [inlined]
 [3] setindex!(V::SubArray{Int64, 1, JuMP.Containers.DenseAxisArray{Int64, 1, Tuple{UnitRange{Int64}}, Tuple{JuMP.Containers._AxisLookup{Tuple{Int64, Int64}}}}, Tuple{Base.Slice{UnitRange{Int64}}}, false}, x::Int64, I::Int64)
   @ Base ./subarray.jl:316
 [4] top-level scope
   @ REPL[44]:1

julia> v[2] = 1
1

julia> v
2-element view(::JuMP.Containers.DenseAxisArray{Int64, 1, Tuple{UnitRange{Int64}}, Tuple{JuMP.Containers._AxisLookup{Tuple{Int64, Int64}}}}, :) with eltype Int64 with indices 2:3:
 1
 0

julia> x
1-dimensional DenseAxisArray{Int64,1,...} with index sets:
    Dimension 1, 2:3
And data, a 2-element Vector{Int64}:
 1
 0

@odow
Copy link
Member Author

odow commented Jun 7, 2022

Seems like over-riding view for all instances would break existing code that currently works.

But I don't know how to intercept a method where the method signature doesn't match.

I guess the fundamental problem is that view assumes:

julia> x
1-dimensional DenseAxisArray{Float64,1,...} with index sets:
    Dimension 1, [:a, :b]
And data, a 2-element Vector{Float64}:
 1.0
 2.0

julia> eachindex(IndexLinear(), x)
2-element Vector{Symbol}:
 :a
 :b

returns an AbtractUnitRange.

@odow
Copy link
Member Author

odow commented Jun 8, 2022

For example, this is clearly wrong:

julia> c = Containers.@container([i=1:4, j=2:3], i + j)
2-dimensional DenseAxisArray{Int64,2,...} with index sets:
    Dimension 1, Base.OneTo(4)
    Dimension 2, 2:3
And data, a 4×2 Matrix{Int64}:
 3  4
 4  5
 5  6
 6  7

julia> eachindex(IndexLinear(), c)
Base.OneTo(8)

@odow odow added the Type: Bug label Jun 8, 2022
@metab0t
Copy link
Contributor

metab0t commented Jul 3, 2022

function Base.view(A::DenseAxisArray{T,N}, idx...) where {T,N}
    new_indices = Base.to_index(A, idx)
    new_axes = _getindex_recurse(A.axes, new_indices, _is_range)
    return DenseAxisArray(view(A.data, new_indices...), new_axes...) 
end

But the code does not work because we use data::Array{T,N} in DenseAxisArray. Maybe we should use AbstractArray to allow SubArray as data storage?

@odow
Copy link
Member Author

odow commented Jul 3, 2022

Maybe we should use AbstractArray to allow SubArray as data storage?

I'd prefer not too, because we'd need a new type parameter in DenseAxisArray. But I'm not really sure what the right solution is.

@odow odow added this to the 1.x milestone Jul 13, 2022
@odow
Copy link
Member Author

odow commented Aug 2, 2022

@metab0t what is the motivation for using a view? When is it useful?

@metab0t
Copy link
Contributor

metab0t commented Aug 2, 2022

For performance reasons:

julia> using JuMP                                                                               
                                                                                                
julia> A = Containers.@container([i=1:100, j=1:200], i+j, container=Array);                     
                                                                                                
julia> DA = Containers.@container([i=1:100, j=1:200], i+j, container=Containers.DenseAxisArray);
                                                                                                
julia> size(A)                                                                                  
(100, 200)                                                                                      
                                                                                                
julia> size(DA)                                                                                 
(100, 200)                                                                                      
                                                                                                
julia> using BenchmarkTools

julia> @btime A[:, 50];
  212.571 ns (2 allocations: 912 bytes)

julia> @btime @view A[:, 50];
  94.044 ns (2 allocations: 64 bytes)

julia> @btime DA[:, 50];
  257.864 ns (5 allocations: 1008 bytes)                                                                

@odow
Copy link
Member Author

odow commented Aug 2, 2022 via email

@metab0t
Copy link
Contributor

metab0t commented Aug 2, 2022

Sometimes we want to extract a row or column of variables to treat it like a 1-d array.
For example:

for i in axes(variables, 1)
    @constraint(LinearAlgebra.dot(f(i), variables[i, :]) <= 0.0)
end

@odow
Copy link
Member Author

odow commented Aug 2, 2022

Less convenient, but you could go something like

for (i, key) in enumerate(axes(variables, 1))
    @constraint(
        model, 
        LinearAlgebra.dot(f(key), view(variables.data, i, :)) <= 0.0,
    )
end

# or

for i in axes(x, 1)
    c = f(i)
    @constraint(model, sum(c[k] * x[i, k] for k in axes(x, 2)) <= 0)
end

@metab0t
Copy link
Contributor

metab0t commented Aug 3, 2022

Yes, we can bypass DenseAxisArray to select the view of its underlying array directly as an alternative.

In fact, view of DenseAxisArray is just a wrapper built upon view of Array which will be nice to have as a shortcut.

The problem is what view of DenseAxisArray should return. Maybe a DenseAxisArray wrapping SubArray?

@odow
Copy link
Member Author

odow commented Aug 3, 2022

It would have to be something like ViewedDenseAxisArray. But that's getting complicated. Do you have a realistic benchmark where this is a problem? (And not just an artificial one. I get the issue. The question is a trade-off in added complexity to JuMP.)

@metab0t
Copy link
Contributor

metab0t commented Aug 3, 2022

Unfortunately, I don't have a benchmark for this issue now.

@odow
Copy link
Member Author

odow commented Dec 7, 2022

I took another look at this. I'm going to close as won't-fix. Supporting views of containers has a number of technical challenges (hard to add without breaking existing code) and is a very rare request.

I will re-open if this comes up again in the future.

@odow
Copy link
Member Author

odow commented Dec 15, 2022

Reopening because we need to make this work for #3151.

@odow
Copy link
Member Author

odow commented Jan 5, 2023

view is now supported for DenseAxisArray.

I don't know if there's an easy way to make it work for SparseAxisArray (you can't call view on a dictionary), so I'm inclined to close this as won't-fix for `SparseAxisArray.

@metab0t
Copy link
Contributor

metab0t commented Jan 6, 2023

I agree. view for DenseAxisArray is sufficient for most use cases.

@odow odow closed this as completed Jan 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category: Containers Related to the Containers submodule Type: Bug
Development

No branches or pull requests

2 participants