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

Tables.jl support #3104

Merged
merged 18 commits into from
Oct 27, 2022
Merged
100 changes: 100 additions & 0 deletions docs/src/manual/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,42 @@ julia> swap.(x)
(1, 2) (2, 2) (3, 2)
```

### Tables

Use [`Containers.table`](@ref) to convert the `Array` into a
[Tables.jl](https://github.com/JuliaData/Tables.jl) compatible
`Vector{<:NamedTuple}`:

```jldoctest containers_array
julia> table = Containers.table(x, :I, :J, :value)
6-element Vector{NamedTuple{(:I, :J, :value), Tuple{Int64, Int64, Tuple{Int64, Int64}}}}:
(I = 1, J = 1, value = (1, 1))
(I = 2, J = 1, value = (2, 1))
(I = 1, J = 2, value = (1, 2))
(I = 2, J = 2, value = (2, 2))
(I = 1, J = 3, value = (1, 3))
(I = 2, J = 3, value = (2, 3))
```

Because it supports the [Tables.jl](https://github.com/JuliaData/Tables.jl)
interface, you can pass it to any function which accepts a table as input:

```jldoctest containers_array
julia> import DataFrames;

julia> DataFrames.DataFrame(table)
6×3 DataFrame
Row │ I J value
│ Int64 Int64 Tuple…
─────┼──────────────────────
1 │ 1 1 (1, 1)
2 │ 2 1 (2, 1)
3 │ 1 2 (1, 2)
4 │ 2 2 (2, 2)
5 │ 1 3 (1, 3)
6 │ 2 3 (2, 3)
```

## DenseAxisArray

A [`Containers.DenseAxisArray`](@ref) is created when the index sets are
Expand Down Expand Up @@ -191,6 +227,38 @@ julia> x.data
(2, :A) (2, :B)
```

### Tables

Use [`Containers.table`](@ref) to convert the `DenseAxisArray` into a
[Tables.jl](https://github.com/JuliaData/Tables.jl) compatible
`Vector{<:NamedTuple}`:

```jldoctest containers_dense
julia> table = Containers.table(x, :I, :J, :value)
4-element Vector{NamedTuple{(:I, :J, :value), Tuple{Int64, Symbol, Tuple{Int64, Symbol}}}}:
(I = 1, J = :A, value = (1, :A))
(I = 2, J = :A, value = (2, :A))
(I = 1, J = :B, value = (1, :B))
(I = 2, J = :B, value = (2, :B))
```

Because it supports the [Tables.jl](https://github.com/JuliaData/Tables.jl)
interface, you can pass it to any function which accepts a table as input:

```jldoctest containers_dense
julia> import DataFrames;

julia> DataFrames.DataFrame(table)
4×3 DataFrame
Row │ I J value
│ Int64 Symbol Tuple…
─────┼────────────────────────
1 │ 1 A (1, :A)
2 │ 2 A (2, :A)
3 │ 1 B (1, :B)
4 │ 2 B (2, :B)
```

## SparseAxisArray

A [`Containers.SparseAxisArray`](@ref) is created when the index sets are
Expand Down Expand Up @@ -252,6 +320,38 @@ JuMP.Containers.SparseAxisArray{Tuple{Symbol, Int64}, 1, Tuple{Int64}} with 2 en
[3] = (:B, 3)
```

### Tables

Use [`Containers.table`](@ref) to convert the `SparseAxisArray` into a
[Tables.jl](https://github.com/JuliaData/Tables.jl) compatible
`Vector{<:NamedTuple}`:

```jldoctest containers_sparse
julia> table = Containers.table(x, :I, :J, :value)
4-element Vector{NamedTuple{(:I, :J, :value), Tuple{Int64, Symbol, Tuple{Int64, Symbol}}}}:
(I = 3, J = :B, value = (3, :B))
(I = 2, J = :A, value = (2, :A))
(I = 2, J = :B, value = (2, :B))
(I = 3, J = :A, value = (3, :A))
```

Because it supports the [Tables.jl](https://github.com/JuliaData/Tables.jl)
interface, you can pass it to any function which accepts a table as input:

```jldoctest containers_sparse
julia> import DataFrames;

julia> DataFrames.DataFrame(table)
4×3 DataFrame
Row │ I J value
│ Int64 Symbol Tuple…
─────┼────────────────────────
1 │ 3 B (3, :B)
2 │ 2 A (2, :A)
3 │ 2 B (2, :B)
4 │ 3 A (3, :A)
```

## Forcing the container type

Pass `container = T` to use `T` as the container. For example:
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Containers
Containers.DenseAxisArray
Containers.SparseAxisArray
Containers.container
Containers.table
Containers.default_container
Containers.@container
Containers.VectorizedProductIterator
Expand Down
10 changes: 10 additions & 0 deletions docs/src/tutorials/linear/diet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ end
# That's a lot of milk and ice cream! And sadly, we only get `0.6` of a
# hamburger.

# We can also use the function [`Containers.table`](@ref) to easily convert the
# result into a DataFrame:

table = Containers.table(value, x, :food, :quantity)
solution = DataFrames.DataFrame(table)

# This makes it easy to perform analyses our solution:

filter!(row -> row.quantity > 0.0, solution)

# ## Problem modification

# JuMP makes it easy to take an existing model and modify it by adding extra
Expand Down
1 change: 1 addition & 0 deletions src/Containers/Containers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ include("nested_iterator.jl")
include("no_duplicate_dict.jl")
include("container.jl")
include("macro.jl")
include("tables.jl")

end
51 changes: 51 additions & 0 deletions src/Containers/tables.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

_rows(x::Array) = zip(eachindex(x), Iterators.product(axes(x)...))

_rows(x::DenseAxisArray) = zip(vec(eachindex(x)), Iterators.product(axes(x)...))

_rows(x::SparseAxisArray) = zip(eachindex(x.data), keys(x.data))

"""
table([f::Function=identity,] x, names::Symbol...)

Applies the function `f` to all elements of the variable container `x`,
returning the result as a `Vector` of `NamedTuple`s, where `names` are used for
the corresponding axis names. If `x` is an `N`-dimensional array, there must be
`N+1` names, so that the last name corresponds to the result of `f(x[i])`.

!!! info
A `Vector` of `NamedTuple`s implements the [Tables.jl](https://github.com/JuliaData/Tables.jl)
interface, and so the result can be used as input for any function
that consumes a 'Tables.jl' compatible source.

## Example

```jldoctest; setup=:(using JuMP)
julia> model = Model();

julia> @variable(model, x[i=1:2, j=i:2] >= 0, start = i+j);

julia> Containers.table(start_value, x, :i, :j, :start)
3-element Vector{NamedTuple{(:i, :j, :start), Tuple{Int64, Int64, Float64}}}:
(i = 1, j = 2, start = 3.0)
(i = 1, j = 1, start = 2.0)
(i = 2, j = 2, start = 4.0)
```
"""
function table(
f::Function,
x::Union{Array,DenseAxisArray,SparseAxisArray},
names::Symbol...,
)
got, want = length(names), ndims(x) + 1
if got != want
error("Invalid number of column names provided: Got $got, expected $want.")
odow marked this conversation as resolved.
Show resolved Hide resolved
end
return [NamedTuple{names}((args..., f(x[i]))) for (i, args) in _rows(x)]
end

table(x, names::Symbol...) = table(identity, x, names...)
123 changes: 123 additions & 0 deletions test/Containers/tables.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

module TestTableInterface

using JuMP
using Test

function runtests()
for name in names(@__MODULE__; all = true)
if startswith("$(name)", "test_")
@testset "$(name)" begin
getfield(@__MODULE__, name)()
end
end
end
return
end

function test_denseaxisarray()
model = Model()
@variable(model, x[i = 4:10, j = 2002:2022] >= 0, start = 0.0)
@test typeof(x) <: Containers.DenseAxisArray
start_table = Containers.table(start_value, x, :index1, :index2, :solution)
T = NamedTuple{(:index1, :index2, :solution),Tuple{Int,Int,Float64}}
@test start_table isa Vector{T}
@test length(start_table) == length(x)
row = first(start_table)
@test row == (index1 = 4, index2 = 2002, solution = 0.0)
x_table = Containers.table(x, :index1, :index2, :variable)
@test x_table[1] == (index1 = 4, index2 = 2002, variable = x[4, 2002])
return
end

function test_array()
model = Model()
@variable(model, x[1:10, 1:5] >= 0, start = 0.0)
@test typeof(x) <: Array{VariableRef}
start_table = Containers.table(start_value, x, :index1, :index2, :solution)
T = NamedTuple{(:index1, :index2, :solution),Tuple{Int,Int,Float64}}
@test start_table isa Vector{T}
@test length(start_table) == length(x)
row = first(start_table)
@test row == (index1 = 1, index2 = 1, solution = 0.0)
x_table = Containers.table(x, :index1, :index2, :variable)
@test x_table[1] == (index1 = 1, index2 = 1, variable = x[1, 1])
return
end

function test_sparseaxisarray()
model = Model()
@variable(model, x[i = 1:10, j = 1:5; i + j <= 8] >= 0, start = 0)
@test typeof(x) <: Containers.SparseAxisArray
start_table = Containers.table(start_value, x, :index1, :index2, :solution)
T = NamedTuple{(:index1, :index2, :solution),Tuple{Int,Int,Float64}}
@test start_table isa Vector{T}
@test length(start_table) == length(x)
@test (index1 = 1, index2 = 1, solution = 0.0) in start_table
x_table = Containers.table(x, :index1, :index2, :variable)
@test (index1 = 1, index2 = 1, variable = x[1, 1]) in x_table
return
end

function test_col_name_error()
model = Model()
@variable(model, x[1:2, 1:2])
@test_throws ErrorException Containers.table(x, :y, :a)
@test_throws ErrorException Containers.table(x, :y, :a, :b, :c)
@test Containers.table(x, :y, :a, :b) isa Vector{<:NamedTuple}
return
end

# Mockup of custom variable type
struct _MockVariable <: JuMP.AbstractVariable
var::JuMP.ScalarVariable
end

struct _MockVariableRef <: JuMP.AbstractVariableRef
vref::VariableRef
end

JuMP.name(v::_MockVariableRef) = JuMP.name(v.vref)

JuMP.owner_model(v::_MockVariableRef) = JuMP.owner_model(v.vref)

JuMP.start_value(v::_MockVariableRef) = JuMP.start_value(v.vref)

struct _Mock end

function JuMP.build_variable(::Function, info::JuMP.VariableInfo, _::_Mock)
return _MockVariable(JuMP.ScalarVariable(info))
end

function JuMP.add_variable(model::Model, x::_MockVariable, name::String)
variable = JuMP.add_variable(model, x.var, name)
return _MockVariableRef(variable)
end

function test_custom_variable()
model = Model()
@variable(
model,
x[i = 1:3, j = 100:102] >= 0,
_Mock(),
container = Containers.DenseAxisArray,
start = 0.0,
)
@test typeof(x) <: Containers.DenseAxisArray
start_table = Containers.table(start_value, x, :index1, :index2, :solution)
T = NamedTuple{(:index1, :index2, :solution),Tuple{Int,Int,Float64}}
@test start_table isa Vector{T}
@test length(start_table) == length(x)
@test (index1 = 1, index2 = 100, solution = 0.0) in start_table
x_table = Containers.table(x, :index1, :index2, :variable)
@test (index1 = 1, index2 = 100, variable = x[1, 100]) in x_table
return
end

end

TestTableInterface.runtests()