-
-
Notifications
You must be signed in to change notification settings - Fork 399
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] add support for Base.getindex(::Container; kwargs...) #3237
Conversation
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## master #3237 +/- ##
==========================================
+ Coverage 98.12% 98.13% +0.01%
==========================================
Files 34 34
Lines 4740 4835 +95
==========================================
+ Hits 4651 4745 +94
- Misses 89 90 +1
... and 4 files with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report in Codecov by Sentry. |
Maybe Furthermore, what do you think about the following convenient features:
|
👍
I think they're a little too magical. This syntax is opt-in and more verbose, so it will be used by people looking for correctness and to make the code more readable. Enforcing an order of arguments helps clarify that that, and so does forcing arguments like |
We don't have a good answer for fixing the usability issue of a user doing model = Model()
@variable(model, x[i=1:2, j = 1:3])
x[i = 1, j = 3] # errors as `x` is a `Matrix`. We have many users that don't care about which type of container is returned by macro and it this kind of erros would be very confusing. What we need to do is to return a @variable(model, x[i=1:2, j = 1:3], lower_bound = i + j, requested_container = Array) However, that is breaking so maybe we need to wait for JuMP v2.0 for this PR ? |
It's confusing, but because it's opt-in, the user can also opt-in to I think changing the default is a bad trade-off. Giving everyone a heavier, slower, more complicated data structure just so some people don't get confused when using kwarg indexing? |
a5e5ce1
to
3bab327
Compare
What is opt-in ? If the user uses keyword indexing with an array and it gets an error, he didn't opt-in for anything and we won't understand a thing
It shouldn't be heavier if every axis is |
Choosing to use We're also not going to change the default before JuMP 2.0, so this is a moot discussion. |
Why do you mean by the default ? The one we advertise in the docs ?
If keyword argument indexing works, the user will just use it without realizing he has opt-in to anything. I don't see how that fixes the usability issue. |
The type created by
It doesn't. But it means they can choose a trade-off. Positional indexing works the same everywhere. Kwarg indexing works for Dense and SparseAxisArray. It's not like you can only use positional with Array and kwarg with {Dense,Sparse}AxisArray. I also think this discussion is somewhat academic. Let's get it out there and see if people complain. |
I think it's the opposite. A company will never add a feature knowing that it will cause confusion to beginners for which the answer is "they have to know better the inner working for JuMP".
If they do, we don't have any other option than breaking changes so not sure I want to take that risk just for fixing an issue raised 5 years ago with no activity for 5 years. |
@mlubin you're going to need to take a look at this one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My proposal is to make this explicitly opt in, i.e., disabled unless you pass keyword_indexing=True
to @variable
or @constraint
. This way we can provide a useful error message when keyword_indexing=True
and the container is Array
.
Let's see if the feature gets used and if anyone likes it, then we can decide what to break for the next major release.
end | ||
|
||
function Base.getindex(x::DenseAxisArrayView, args...; kwargs...) | ||
if !isempty(kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getindex
is performance sensitive. Is there any impact from this extra !isempty(kwargs)
in the case of all positional arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. It gets compiled away:
shell> git status
On branch od/container-kwarg-getindex
Your branch is up to date with 'origin/od/container-kwarg-getindex'.
nothing to commit, working tree clean
julia> using JuMP
julia> using BenchmarkTools
julia> model = Model()
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: AUTOMATIC
CachingOptimizer state: NO_OPTIMIZER
Solver name: No optimizer attached.
julia> @variable(model, x[i=2:3, j=4:5])
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, 2:3
Dimension 2, 4:5
And data, a 2×2 Matrix{VariableRef}:
x[2,4] x[2,5]
x[3,4] x[3,5]
julia> @btime getindex($x, 2, 4)
3.466 ns (0 allocations: 0 bytes)
x[2,4]
julia> exit()
(base) oscar@Oscars-MBP JuMP % git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
(base) oscar@Oscars-MBP JuMP % julia --project=.
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.6.7 (2022-07-19)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using JuMP
[ Info: Precompiling JuMP [4076af6c-e467-56ae-b986-b466b2749572]
julia> using BenchmarkTools
julia> model = Model()
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: AUTOMATIC
CachingOptimizer state: NO_OPTIMIZER
Solver name: No optimizer attached.
julia> @variable(model, x[i=2:3, j=4:5])
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, 2:3
Dimension 2, 4:5
And data, a 2×2 Matrix{VariableRef}:
x[2,4] x[2,5]
x[3,4] x[3,5]
julia> @btime getindex($x, 2, 4)
3.725 ns (0 allocations: 0 bytes)
x[2,4]
There's still room to improve the kwarg
option, but let's settle on the syntax/opt-in stuff first:
julia> @btime getindex($x; i = 2, j = 4)
33.646 ns (1 allocation: 32 bytes)
x[2,4]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is kwarg indexing fundamentally 10x slower than positional indexing? If that's true we might want to reconsider the syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, if you change the method to a singular getindex(x; kwargs...)
it comes down to 12ns and 0 allocations, but then I needed to add a bunch of additional methods to avoid ambiguities. Left as-is for now while we sort the syntax.
Keyword indexing in general does have a slight performance hit though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keyword indexing in general does have a slight performance hit though.
In fact, keyword indexing can be as fast as Cartesian indexing if the tuple of names is encoded into the customized array type, but it is obviously not an option for JuMP now.
That would resolve the usability issue. Maybe this option can be at the level of the model ? |
If it's at the level of the model, then does this mean we print a warning or error for every |
I looked into the flag option, and it's actually pretty complicated to do with a model-level flag, because the value of the flag needs to be available at macro-expansion time. so One option could be some global constant and require users to opt-in with But in general, I think the option is over-kill. Is it really too much to ask a user who chooses to use keyword indexing to understand the difference between an julia> model = Model();
julia> @variable(model, x[i=1:2, j=1:2])
2×2 Matrix{VariableRef}:
x[1,1] x[1,2]
x[2,1] x[2,2]
julia> @variable(model, y[i=1:2, j=1:2], container = DenseAxisArray)
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, Base.OneTo(2)
Dimension 2, Base.OneTo(2)
And data, a 2×2 Matrix{VariableRef}:
y[1,1] y[1,2]
y[2,1] y[2,2]
julia> x[i=1, j=1]
ERROR: MethodError: no method matching getindex(::Matrix{VariableRef}; i=1, j=1)
Closest candidates are:
getindex(::Array, ::Int64) at array.jl:805 got unsupported keyword arguments "i", "j"
getindex(::Array, ::Int64, ::Int64, ::Int64...) at array.jl:806 got unsupported keyword arguments "i", "j"
getindex(::Array, ::UnitRange{Int64}) at array.jl:809 got unsupported keyword arguments "i", "j"
...
Stacktrace:
[1] top-level scope
@ REPL[8]:1
julia> y[i=1, j=1]
y[1,1] |
1698ebe
to
f208e55
Compare
|
It seems weird to throw an error there, but not if And this is still premised on the fact that users will be confused, which I'm not sure they will be. If we get complaints, then I think it's something that can be resolved with documentation. Not with warnings or Spitballing other ideas:We could make something like |
They could be used for readability/self-documented code as well.
I find the keyword option less heavy syntax.
Is this meant for Thinking more about it, users might still find keyword indexing being used in the middle of JuMP code and then trying it and be confused without knowing about the Returning a |
We seem to be stalemating. To summarize where we're at:
The options are:
The warning is hard, because it'd be hard to distinguish The opt-in is also hard because it would require the macro code to depend on the run-time setting in If that doesn't work, we're either left with 1 or 4, which probably means 4 if we don't agree. |
IMO it's not at all weird to warn when there are unused variables.
Agreed.
This change seems very disruptive for people who are already happily using Array containers. If we do this we should be confident about how useful the keyword indexing is. It's hard to have this confidence before there are people using it in the wild and giving us feedback. So, I'm in favor of the more explicit opt-in version. It's ok with me if it's a flag to the macro. We don't need a complicated model-level flag. If the feedback is, "this feature is great but it's too annoying to opt in", then maybe we'd make it the default and deal with the fallout. |
What about a global flag via |
A global flag means this feature can't be used in library code. |
I guess I was imagining that this flag was going to be used in user-level code only Okay. Let me do the macro- and model-level flag... |
The demo is julia> using JuMP
julia> model = Model();
julia> @variable(model, x[i = 2:3])
1-dimensional DenseAxisArray{VariableRef,1,...} with index sets:
Dimension 1, 2:3
And data, a 2-element Vector{VariableRef}:
x[2]
x[3]
julia> x[i = 2]
ERROR: Keyword indexing is disabled. To enable, pass `enable_keyword_indexing = true` to the `Containers.@container` macro, or call `JuMP.enable_container_keyword_indexing(model, true)` before calling any JuMP macros like `@variable`.
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:33
[2] _check_keyword_indexing_allowed(#unused#::Nothing)
@ JuMP.Containers ~/.julia/dev/JuMP/src/Containers/Containers.jl:31
[3] #_kwargs_to_args#17
@ ~/.julia/dev/JuMP/src/Containers/DenseAxisArray.jl:356 [inlined]
[4] #getindex#20
@ ~/.julia/dev/JuMP/src/Containers/DenseAxisArray.jl:375 [inlined]
[5] top-level scope
@ REPL[5]:1
julia> model = Model();
julia> enable_container_keyword_indexing(model, true)
julia> @variable(model, x[i = 2:3])
1-dimensional DenseAxisArray{VariableRef,1,...} with index sets:
Dimension 1, 2:3
And data, a 2-element Vector{VariableRef}:
x[2]
x[3]
julia> x[i = 2]
x[2] |
4f3ac10
to
dbfdca7
Compare
Performance issue is still unresolved, #3237 (comment), but I'll fix that in a separate PR. This PR is already quite complicated. |
) | ||
end | ||
|
||
function Base.getindex(x::Array{<:AbstractJuMPScalar}; kwargs...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that called when you do getindex(x)
without any keyword argument ? There should be at least one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean to call getindex(x)
?
Even without JuMP:
julia> x = rand(3)
3-element Vector{Float64}:
0.8542832661650948
0.36511705612410084
0.12183318054742132
julia> x[]
ERROR: BoundsError: attempt to access 3-element Vector{Float64} at index []
Stacktrace:
[1] throw_boundserror(A::Vector{Float64}, I::Tuple{})
@ Base ./abstractarray.jl:651
[2] checkbounds
@ ./abstractarray.jl:616 [inlined]
[3] _getindex
@ ./abstractarray.jl:1196 [inlined]
[4] getindex(::Vector{Float64})
@ Base ./abstractarray.jl:1170
[5] top-level scope
@ REPL[451]:1
But yeah, I guess we could throw a bounds error if there are no kwargs and we get dispatched here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
Closes #1291
There are still a few issues to work through: