From 4658c2ef7b59d50bd00cdf583a782b4f1e62ed5b Mon Sep 17 00:00:00 2001 From: Truls Flatberg Date: Sun, 2 Oct 2022 23:15:30 +0200 Subject: [PATCH 01/18] Initial work on Tables support for JuMP --- Project.toml | 1 + src/JuMP.jl | 4 ++ src/tables.jl | 163 +++++++++++++++++++++++++++++++++++++++++++++++++ test/tables.jl | 97 +++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 src/tables.jl create mode 100644 test/tables.jl diff --git a/Project.toml b/Project.toml index 363dbd698e7..797a467fe20 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] MathOptInterface = "1.3.0" diff --git a/src/JuMP.jl b/src/JuMP.jl index 7252fe83554..52e8331cc0b 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1360,6 +1360,10 @@ include("callbacks.jl") include("file_formats.jl") include("feasibility_checker.jl") +using Tables +include("tables.jl") + + # MOI contains a number of Enums that are often accessed by users such as # `MOI.OPTIMAL`. This piece of code re-exports them from JuMP so that users can # use: `MOI.OPTIMAL`, `JuMP.OPTIMAL`, or `using JuMP; OPTIMAL`. diff --git a/src/tables.jl b/src/tables.jl new file mode 100644 index 00000000000..d75af7dd27d --- /dev/null +++ b/src/tables.jl @@ -0,0 +1,163 @@ +abstract type _SolutionTable end + +Tables.istable(::Type{<:_SolutionTable}) = true +Tables.rowaccess(::Type{<:_SolutionTable}) = true + +_column_names(t::_SolutionTable) = getfield(t, :column_names) +_lookup(t::_SolutionTable) = getfield(t, :lookup) + +Base.eltype(::_SolutionTable) = SolutionRow +Base.length(t::_SolutionTable) = length(t.var) + +struct _SolutionRow <: Tables.AbstractRow + index_vals::Any + sol_val::Number + source::_SolutionTable +end + +function Tables.getcolumn(s::_SolutionRow, i::Int) + if i > length(getfield(s, :index_vals)) + return getfield(s, :sol_val) + end + return getfield(s, :index_vals)[i] +end + +function Tables.getcolumn(s::_SolutionRow, nm::Symbol) + i = _lookup(getfield(s, :source))[nm] + if i > length(getfield(s, :index_vals)) + return getfield(s, :sol_val) + end + return getfield(s, :index_vals)[i] +end + +Tables.columnnames(s::_SolutionRow) = _column_names(getfield(s, :source)) + +struct _SolutionTableDense <: _SolutionTable + column_names::Vector{Symbol} + lookup::Dict{Symbol,Int} + index_lookup::Dict + var::Containers.DenseAxisArray +end + +function _SolutionTableDense( + v::Containers.DenseAxisArray{VariableRef,N,Ax,L}, + name, + colnames..., +) where {N,Ax,L} + if length(colnames) < length(axes(v)) + error("Not enough column names provided") + end + if length(v) > 0 && !has_values(first(v).model) + error("No solution values available for variable") + end + all_names = vcat(colnames..., name) + lookup = Dict(nm => i for (i, nm) in enumerate(all_names)) + index_lookup = Dict() + for (i, ax) in enumerate(axes(v)) + index_lookup[i] = collect(ax) + end + return _SolutionTableDense(all_names, lookup, index_lookup, v) +end + +function Base.iterate(t::_SolutionTableDense, state = nothing) + next = + isnothing(state) ? iterate(eachindex(t.var)) : + iterate(eachindex(t.var), state) + next === nothing && return nothing + index = next[1] + index_vals = [t.index_lookup[i][index[i]] for i in 1:length(index)] + return _SolutionRow(index_vals, JuMP.value(t.var[next[1]]), t), next[2] +end + +function table( + var::Containers.DenseAxisArray{JuMP.VariableRef,N,Ax,L}, + name, + colnames..., +) where {N,Ax,L} + return _SolutionTableDense(var, name, colnames...) +end + +struct _SolutionTableArray <: _SolutionTable + column_names::Vector{Symbol} + lookup::Dict{Symbol,Int} + index_lookup::Dict + var::Array{VariableRef} +end + +function _SolutionTableArray( + v::Array{VariableRef}, + name, + colnames..., +) + if length(colnames) < length(axes(v)) + error("Not enough column names provided") + end + if length(v) > 0 && !has_values(first(v).model) + error("No solution values available for variable") + end + all_names = vcat(colnames..., name) + lookup = Dict(nm => i for (i, nm) in enumerate(all_names)) + index_lookup = Dict() + for (i, ax) in enumerate(axes(v)) + index_lookup[i] = collect(ax) + end + return _SolutionTableArray(all_names, lookup, index_lookup, v) +end + +function Base.iterate(t::_SolutionTableArray, state = nothing) + next = + isnothing(state) ? iterate(CartesianIndices(t.var)) : + iterate(CartesianIndices(t.var), state) + next === nothing && return nothing + index = next[1] + index_vals = [t.index_lookup[i][index[i]] for i in 1:length(index)] + return _SolutionRow(index_vals, JuMP.value(t.var[next[1]]), t), next[2] +end + +function table( + var::Array{VariableRef}, + name, + colnames..., +) + return _SolutionTableArray(var, name, colnames...) +end + +struct _SolutionTableSparse <: _SolutionTable + column_names::Vector{Symbol} + lookup::Dict{Symbol,Int} + var::Containers.SparseAxisArray +end + +function _SolutionTableSparse( + v::Containers.SparseAxisArray{VariableRef,N,K}, + name, + colnames..., +) where {N,K} + if length(colnames) < N + error("Not enough column names provided") + end + if length(v) > 0 && !has_values(first(v).model) + error("No solution values available for variable") + end + all_names = vcat(colnames..., name) + lookup = Dict(nm => i for (i, nm) in enumerate(all_names)) + return _SolutionTableSparse(all_names, lookup, v) +end + +function Base.iterate(t::_SolutionTableSparse, state = nothing) + next = + isnothing(state) ? iterate(eachindex(t.var)) : + iterate(eachindex(t.var), state) + next === nothing && return nothing + return _SolutionRow(next[1], JuMP.value(t.var[next[1]]), t), next[2] +end + +function table( + var::Containers.SparseAxisArray{VariableRef,N,K}, + name, + colnames..., +) where {N,K} + return _SolutionTableSparse(var, name, colnames...) +end + + diff --git a/test/tables.jl b/test/tables.jl new file mode 100644 index 00000000000..2d0d77ff407 --- /dev/null +++ b/test/tables.jl @@ -0,0 +1,97 @@ +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) + @objective(model, Min, sum(x)) + set_optimizer( + model, + () -> MOI.Utilities.MockOptimizer( + MOI.Utilities.Model{Float64}(); + eval_objective_value = false, + ), + ) + optimize!(model) + mockoptimizer = JuMP.unsafe_backend(model) + MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) + MOI.set(mockoptimizer, MOI.ResultCount(), 1) + MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) + + for ind in eachindex(x) + MOI.set(mockoptimizer, MOI.VariablePrimal(), JuMP.optimizer_index(x[ind]), 0.0) + end + + t = JuMP.table(x, :solution, :index1, :index2) + +end + +function test_array() + model = Model() + @variable(model, x[1:10, 1:5] >= 0) + @test typeof(x) <: Array{VariableRef} + + @objective(model, Min, sum(x)) + set_optimizer( + model, + () -> MOI.Utilities.MockOptimizer( + MOI.Utilities.Model{Float64}(); + eval_objective_value = false, + ), + ) + optimize!(model) + mockoptimizer = JuMP.unsafe_backend(model) + MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) + MOI.set(mockoptimizer, MOI.ResultCount(), 1) + MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) + + for ind in eachindex(x) + MOI.set(mockoptimizer, MOI.VariablePrimal(), JuMP.optimizer_index(x[ind]), 0.0) + end + + t = JuMP.table(x, :solution, :index1, :index2) +end + +function test_sparseaxisarray() + model = Model() + @variable(model, x[i=1:10, j=1:5; i + j <= 8] >= 0) + @test typeof(x) <: Containers.SparseAxisArray + + @objective(model, Min, sum(x)) + set_optimizer( + model, + () -> MOI.Utilities.MockOptimizer( + MOI.Utilities.Model{Float64}(); + eval_objective_value = false, + ), + ) + optimize!(model) + mockoptimizer = JuMP.unsafe_backend(model) + MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) + MOI.set(mockoptimizer, MOI.ResultCount(), 1) + MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) + + for ind in eachindex(x) + MOI.set(mockoptimizer, MOI.VariablePrimal(), JuMP.optimizer_index(x[ind]), 0.0) + end + + t = JuMP.table(x, :solution, :index1, :index2) +end + +end \ No newline at end of file From 08a86b2d98e44b597d77a27b75d5a7fe504f09ae Mon Sep 17 00:00:00 2001 From: Truls Flatberg Date: Tue, 4 Oct 2022 21:19:50 +0200 Subject: [PATCH 02/18] Refactor dense tables, support AbstractVariableRef --- src/JuMP.jl | 1 - src/tables.jl | 107 +++++++++++++++++++++++++------------------------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/JuMP.jl b/src/JuMP.jl index 52e8331cc0b..20a85474400 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1363,7 +1363,6 @@ include("feasibility_checker.jl") using Tables include("tables.jl") - # MOI contains a number of Enums that are often accessed by users such as # `MOI.OPTIMAL`. This piece of code re-exports them from JuMP so that users can # use: `MOI.OPTIMAL`, `JuMP.OPTIMAL`, or `using JuMP; OPTIMAL`. diff --git a/src/tables.jl b/src/tables.jl index d75af7dd27d..b7ce992ba3d 100644 --- a/src/tables.jl +++ b/src/tables.jl @@ -6,7 +6,7 @@ Tables.rowaccess(::Type{<:_SolutionTable}) = true _column_names(t::_SolutionTable) = getfield(t, :column_names) _lookup(t::_SolutionTable) = getfield(t, :lookup) -Base.eltype(::_SolutionTable) = SolutionRow +Base.eltype(::_SolutionTable) = _SolutionRow Base.length(t::_SolutionTable) = length(t.var) struct _SolutionRow <: Tables.AbstractRow @@ -32,22 +32,32 @@ end Tables.columnnames(s::_SolutionRow) = _column_names(getfield(s, :source)) -struct _SolutionTableDense <: _SolutionTable +struct _SolutionTableDense{C} <: _SolutionTable column_names::Vector{Symbol} lookup::Dict{Symbol,Int} index_lookup::Dict - var::Containers.DenseAxisArray + var::C +end + +function Base.iterate(t::_SolutionTableDense, state = nothing) + next = + isnothing(state) ? iterate(CartesianIndices(t.var)) : + iterate(CartesianIndices(t.var), state) + next === nothing && return nothing + index = next[1] + index_vals = [t.index_lookup[i][index[i]] for i in 1:length(index)] + return _SolutionRow(index_vals, JuMP.value(t.var[next[1]]), t), next[2] end function _SolutionTableDense( - v::Containers.DenseAxisArray{VariableRef,N,Ax,L}, + v::Containers.DenseAxisArray{T,N,Ax,L}, name, colnames..., -) where {N,Ax,L} - if length(colnames) < length(axes(v)) +) where {T<:AbstractVariableRef,N,Ax,L} + if length(colnames) < N error("Not enough column names provided") end - if length(v) > 0 && !has_values(first(v).model) + if length(v) > 0 && !has_values(owner_model(first(v))) error("No solution values available for variable") end all_names = vcat(colnames..., name) @@ -59,40 +69,43 @@ function _SolutionTableDense( return _SolutionTableDense(all_names, lookup, index_lookup, v) end -function Base.iterate(t::_SolutionTableDense, state = nothing) - next = - isnothing(state) ? iterate(eachindex(t.var)) : - iterate(eachindex(t.var), state) - next === nothing && return nothing - index = next[1] - index_vals = [t.index_lookup[i][index[i]] for i in 1:length(index)] - return _SolutionRow(index_vals, JuMP.value(t.var[next[1]]), t), next[2] -end - -function table( - var::Containers.DenseAxisArray{JuMP.VariableRef,N,Ax,L}, +""" + solution_table(var::DenseAxisArray, name, colnames...) + +Returns the solution values of the variable container `var` as a table +that implements the `Tables.jl` interface. + +The table will have one column for each index and a column with the +corresponding solution value. The name of the column with the solution +value is provided by `name`, while `colnames` provides the name of the +index columns. + +## Example +```julia +model = Model() +@variable(model, x[1:10, 2000:2020] >= 0) +[...] +optimize!(model) +tbl = solution_table(x, :value, :car, :year) +``` +""" +function solution_table( + var::Containers.DenseAxisArray{T,N,Ax,L}, name, colnames..., -) where {N,Ax,L} +) where {T<:AbstractVariableRef,N,Ax,L} return _SolutionTableDense(var, name, colnames...) end -struct _SolutionTableArray <: _SolutionTable - column_names::Vector{Symbol} - lookup::Dict{Symbol,Int} - index_lookup::Dict - var::Array{VariableRef} -end - -function _SolutionTableArray( - v::Array{VariableRef}, +function _SolutionTableDense( + v::Array{T}, name, colnames..., -) +) where {T<:AbstractVariableRef} if length(colnames) < length(axes(v)) error("Not enough column names provided") end - if length(v) > 0 && !has_values(first(v).model) + if length(v) > 0 && !has_values(owner_model(first(v))) error("No solution values available for variable") end all_names = vcat(colnames..., name) @@ -101,25 +114,15 @@ function _SolutionTableArray( for (i, ax) in enumerate(axes(v)) index_lookup[i] = collect(ax) end - return _SolutionTableArray(all_names, lookup, index_lookup, v) -end - -function Base.iterate(t::_SolutionTableArray, state = nothing) - next = - isnothing(state) ? iterate(CartesianIndices(t.var)) : - iterate(CartesianIndices(t.var), state) - next === nothing && return nothing - index = next[1] - index_vals = [t.index_lookup[i][index[i]] for i in 1:length(index)] - return _SolutionRow(index_vals, JuMP.value(t.var[next[1]]), t), next[2] + return _SolutionTableDense(all_names, lookup, index_lookup, v) end -function table( - var::Array{VariableRef}, +function solution_table( + var::Array{T}, name, colnames..., -) - return _SolutionTableArray(var, name, colnames...) +) where {T<:AbstractVariableRef} + return _SolutionTableDense(var, name, colnames...) end struct _SolutionTableSparse <: _SolutionTable @@ -129,10 +132,10 @@ struct _SolutionTableSparse <: _SolutionTable end function _SolutionTableSparse( - v::Containers.SparseAxisArray{VariableRef,N,K}, + v::Containers.SparseAxisArray{T,N,K}, name, colnames..., -) where {N,K} +) where {T<:AbstractVariableRef,N,K} if length(colnames) < N error("Not enough column names provided") end @@ -152,12 +155,10 @@ function Base.iterate(t::_SolutionTableSparse, state = nothing) return _SolutionRow(next[1], JuMP.value(t.var[next[1]]), t), next[2] end -function table( - var::Containers.SparseAxisArray{VariableRef,N,K}, +function solution_table( + var::Containers.SparseAxisArray{T,N,K}, name, colnames..., -) where {N,K} +) where {T<:AbstractVariableRef,N,K} return _SolutionTableSparse(var, name, colnames...) end - - From df9c9cb0bdd838735198c06300c06257b0d9ddfe Mon Sep 17 00:00:00 2001 From: Truls Flatberg Date: Tue, 4 Oct 2022 21:20:15 +0200 Subject: [PATCH 03/18] Extend test coverage --- test/tables.jl | 158 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 18 deletions(-) diff --git a/test/tables.jl b/test/tables.jl index 2d0d77ff407..5993a8b6519 100644 --- a/test/tables.jl +++ b/test/tables.jl @@ -1,6 +1,7 @@ module TestTableInterface using JuMP +using Tables using Test function runtests() @@ -16,7 +17,7 @@ end function test_denseaxisarray() model = Model() - @variable(model, x[i = 4:10, j=2002:2022] >= 0) + @variable(model, x[i = 4:10, j = 2002:2022] >= 0) @objective(model, Min, sum(x)) set_optimizer( model, @@ -31,20 +32,36 @@ function test_denseaxisarray() MOI.set(mockoptimizer, MOI.ResultCount(), 1) MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - + for ind in eachindex(x) - MOI.set(mockoptimizer, MOI.VariablePrimal(), JuMP.optimizer_index(x[ind]), 0.0) + MOI.set( + mockoptimizer, + MOI.VariablePrimal(), + JuMP.optimizer_index(x[ind]), + 0.0, + ) end - - t = JuMP.table(x, :solution, :index1, :index2) - + + tbl = JuMP.solution_table(x, :solution, :index1, :index2) + @test Tables.istable(typeof(tbl)) + @test Tables.rowaccess(typeof(tbl)) + + tblrow = first(tbl) + @test eltype(tbl) == typeof(tblrow) + @test Tables.getcolumn(tblrow, :index1) == 4 + @test Tables.getcolumn(tblrow, 1) == 4 + @test tblrow.index1 == 4 + @test propertynames(tblrow) == [:index1, :index2, :solution] + + rows = collect(tbl) + @test length(rows) == length(tbl) end function test_array() model = Model() @variable(model, x[1:10, 1:5] >= 0) @test typeof(x) <: Array{VariableRef} - + @objective(model, Min, sum(x)) set_optimizer( model, @@ -59,19 +76,36 @@ function test_array() MOI.set(mockoptimizer, MOI.ResultCount(), 1) MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - + for ind in eachindex(x) - MOI.set(mockoptimizer, MOI.VariablePrimal(), JuMP.optimizer_index(x[ind]), 0.0) + MOI.set( + mockoptimizer, + MOI.VariablePrimal(), + JuMP.optimizer_index(x[ind]), + 0.0, + ) end - - t = JuMP.table(x, :solution, :index1, :index2) + + tbl = JuMP.solution_table(x, :solution, :index1, :index2) + + @test Tables.istable(typeof(tbl)) + @test Tables.rowaccess(typeof(tbl)) + + tblrow = first(tbl) + @test eltype(tbl) == typeof(tblrow) + @test Tables.getcolumn(tblrow, :index1) == 1 + @test Tables.getcolumn(tblrow, 1) == 1 + @test tblrow.index1 == 1 + @test propertynames(tblrow) == [:index1, :index2, :solution] + + rows = collect(tbl) + @test length(rows) == length(tbl) end function test_sparseaxisarray() model = Model() - @variable(model, x[i=1:10, j=1:5; i + j <= 8] >= 0) + @variable(model, x[i = 1:10, j = 1:5; i + j <= 8] >= 0) @test typeof(x) <: Containers.SparseAxisArray - @objective(model, Min, sum(x)) set_optimizer( model, @@ -86,12 +120,100 @@ function test_sparseaxisarray() MOI.set(mockoptimizer, MOI.ResultCount(), 1) MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - + + for ind in eachindex(x) + MOI.set( + mockoptimizer, + MOI.VariablePrimal(), + JuMP.optimizer_index(x[ind]), + 0.0, + ) + end + + tbl = JuMP.solution_table(x, :solution, :index1, :index2) + @test Tables.istable(typeof(tbl)) + @test Tables.rowaccess(typeof(tbl)) + + tblrow = first(tbl) + @test eltype(tbl) == typeof(tblrow) + @test Tables.getcolumn(tblrow, :index1) == 1 + @test Tables.getcolumn(tblrow, 1) == 1 + @test tblrow.index1 == 1 + @test propertynames(tblrow) == [:index1, :index2, :solution] + + rows = collect(tbl) + @test length(rows) == length(tbl) +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.value(v::_MockVariableRef) = JuMP.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 + ) + + @objective(model, Min, 0) + set_optimizer( + model, + () -> MOI.Utilities.MockOptimizer( + MOI.Utilities.Model{Float64}(); + eval_objective_value = false, + ), + ) + optimize!(model) + mockoptimizer = JuMP.unsafe_backend(model) + MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) + MOI.set(mockoptimizer, MOI.ResultCount(), 1) + MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) + for ind in eachindex(x) - MOI.set(mockoptimizer, MOI.VariablePrimal(), JuMP.optimizer_index(x[ind]), 0.0) + MOI.set( + mockoptimizer, + MOI.VariablePrimal(), + JuMP.optimizer_index(x[ind].vref), + 0.0, + ) end - - t = JuMP.table(x, :solution, :index1, :index2) + + tbl = JuMP.solution_table(x, :solution, :index1, :index2) + @test Tables.istable(typeof(tbl)) + @test Tables.rowaccess(typeof(tbl)) + + tblrow = first(tbl) + @test eltype(tbl) == typeof(tblrow) + @test Tables.getcolumn(tblrow, :index1) == 1 + @test Tables.getcolumn(tblrow, 1) == 1 + @test tblrow.index1 == 1 + @test propertynames(tblrow) == [:index1, :index2, :solution] +end + end -end \ No newline at end of file +TestTableInterface.runtests() From 6aab4f6e0fea8d36093ac56f5882934d0a45c9ad Mon Sep 17 00:00:00 2001 From: Truls Flatberg Date: Wed, 5 Oct 2022 17:58:42 +0200 Subject: [PATCH 04/18] Change to import of Tables package --- src/JuMP.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JuMP.jl b/src/JuMP.jl index 20a85474400..be3f86b2ca8 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1360,7 +1360,7 @@ include("callbacks.jl") include("file_formats.jl") include("feasibility_checker.jl") -using Tables +import Tables include("tables.jl") # MOI contains a number of Enums that are often accessed by users such as From 0c409ad9027f65cb385d72d886e1ef4bde1251f3 Mon Sep 17 00:00:00 2001 From: Truls Flatberg Date: Sun, 16 Oct 2022 10:17:32 +0200 Subject: [PATCH 05/18] Support Tables interface by Vector of NamedTuples --- Project.toml | 1 - src/JuMP.jl | 2 - src/tables.jl | 165 ++++++++----------------------------------------- test/tables.jl | 50 +++++++-------- 4 files changed, 46 insertions(+), 172 deletions(-) diff --git a/Project.toml b/Project.toml index 797a467fe20..363dbd698e7 100644 --- a/Project.toml +++ b/Project.toml @@ -10,7 +10,6 @@ MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] MathOptInterface = "1.3.0" diff --git a/src/JuMP.jl b/src/JuMP.jl index be3f86b2ca8..7dce4e1066f 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1359,8 +1359,6 @@ include("lp_sensitivity2.jl") include("callbacks.jl") include("file_formats.jl") include("feasibility_checker.jl") - -import Tables include("tables.jl") # MOI contains a number of Enums that are often accessed by users such as diff --git a/src/tables.jl b/src/tables.jl index b7ce992ba3d..b98695f2447 100644 --- a/src/tables.jl +++ b/src/tables.jl @@ -1,84 +1,29 @@ -abstract type _SolutionTable end - -Tables.istable(::Type{<:_SolutionTable}) = true -Tables.rowaccess(::Type{<:_SolutionTable}) = true - -_column_names(t::_SolutionTable) = getfield(t, :column_names) -_lookup(t::_SolutionTable) = getfield(t, :lookup) - -Base.eltype(::_SolutionTable) = _SolutionRow -Base.length(t::_SolutionTable) = length(t.var) - -struct _SolutionRow <: Tables.AbstractRow - index_vals::Any - sol_val::Number - source::_SolutionTable +function _row_iterator(x::Union{Array,Containers.DenseAxisArray}) + return zip(eachindex(x), Iterators.product(axes(x)...)) end -function Tables.getcolumn(s::_SolutionRow, i::Int) - if i > length(getfield(s, :index_vals)) - return getfield(s, :sol_val) - end - return getfield(s, :index_vals)[i] -end - -function Tables.getcolumn(s::_SolutionRow, nm::Symbol) - i = _lookup(getfield(s, :source))[nm] - if i > length(getfield(s, :index_vals)) - return getfield(s, :sol_val) - end - return getfield(s, :index_vals)[i] +function _row_iterator(x::Containers.SparseAxisArray) + return zip(eachindex(x.data), keys(x.data)) end -Tables.columnnames(s::_SolutionRow) = _column_names(getfield(s, :source)) +_columns(x::Union{Array, Containers.DenseAxisArray}) = length(axes(x)) +_columns(x::Containers.SparseAxisArray{T,N,K}) where {T,N,K} = N -struct _SolutionTableDense{C} <: _SolutionTable - column_names::Vector{Symbol} - lookup::Dict{Symbol,Int} - index_lookup::Dict - var::C -end - -function Base.iterate(t::_SolutionTableDense, state = nothing) - next = - isnothing(state) ? iterate(CartesianIndices(t.var)) : - iterate(CartesianIndices(t.var), state) - next === nothing && return nothing - index = next[1] - index_vals = [t.index_lookup[i][index[i]] for i in 1:length(index)] - return _SolutionRow(index_vals, JuMP.value(t.var[next[1]]), t), next[2] -end -function _SolutionTableDense( - v::Containers.DenseAxisArray{T,N,Ax,L}, - name, - colnames..., -) where {T<:AbstractVariableRef,N,Ax,L} - if length(colnames) < N - error("Not enough column names provided") - end - if length(v) > 0 && !has_values(owner_model(first(v))) - error("No solution values available for variable") - end - all_names = vcat(colnames..., name) - lookup = Dict(nm => i for (i, nm) in enumerate(all_names)) - index_lookup = Dict() - for (i, ax) in enumerate(axes(v)) - index_lookup[i] = collect(ax) - end - return _SolutionTableDense(all_names, lookup, index_lookup, v) +function table(x, name::Symbol, col_names::Symbol...) + return table(identity, x, name, col_names...) end """ - solution_table(var::DenseAxisArray, name, colnames...) + table(f::Function, x, name, colnames...) -Returns the solution values of the variable container `var` as a table -that implements the `Tables.jl` interface. +Applies the function `f` to all elements of the variable container `x` +and returns the result as a `Vector` of `NamedTuple`s using the provided +names. -The table will have one column for each index and a column with the -corresponding solution value. The name of the column with the solution -value is provided by `name`, while `colnames` provides the name of the -index columns. +A `Vector` of `NamedTuple`s implements the 'Tables.jl' interface +and can be used as input for anything that consumes a 'Tables.jl' +compatible source. ## Example ```julia @@ -86,79 +31,19 @@ model = Model() @variable(model, x[1:10, 2000:2020] >= 0) [...] optimize!(model) -tbl = solution_table(x, :value, :car, :year) +tbl = table(value, x, :solution_value, :car, :year) ``` """ -function solution_table( - var::Containers.DenseAxisArray{T,N,Ax,L}, - name, - colnames..., -) where {T<:AbstractVariableRef,N,Ax,L} - return _SolutionTableDense(var, name, colnames...) -end - -function _SolutionTableDense( - v::Array{T}, - name, - colnames..., -) where {T<:AbstractVariableRef} - if length(colnames) < length(axes(v)) +function table( + f::Function, + x::Union{Array,Containers.DenseAxisArray,Containers.SparseAxisArray}, + name::Symbol, + col_names::Symbol..., +) + if length(col_names) < _columns(x) error("Not enough column names provided") end - if length(v) > 0 && !has_values(owner_model(first(v))) - error("No solution values available for variable") - end - all_names = vcat(colnames..., name) - lookup = Dict(nm => i for (i, nm) in enumerate(all_names)) - index_lookup = Dict() - for (i, ax) in enumerate(axes(v)) - index_lookup[i] = collect(ax) - end - return _SolutionTableDense(all_names, lookup, index_lookup, v) -end - -function solution_table( - var::Array{T}, - name, - colnames..., -) where {T<:AbstractVariableRef} - return _SolutionTableDense(var, name, colnames...) -end - -struct _SolutionTableSparse <: _SolutionTable - column_names::Vector{Symbol} - lookup::Dict{Symbol,Int} - var::Containers.SparseAxisArray -end - -function _SolutionTableSparse( - v::Containers.SparseAxisArray{T,N,K}, - name, - colnames..., -) where {T<:AbstractVariableRef,N,K} - if length(colnames) < N - error("Not enough column names provided") - end - if length(v) > 0 && !has_values(first(v).model) - error("No solution values available for variable") - end - all_names = vcat(colnames..., name) - lookup = Dict(nm => i for (i, nm) in enumerate(all_names)) - return _SolutionTableSparse(all_names, lookup, v) -end - -function Base.iterate(t::_SolutionTableSparse, state = nothing) - next = - isnothing(state) ? iterate(eachindex(t.var)) : - iterate(eachindex(t.var), state) - next === nothing && return nothing - return _SolutionRow(next[1], JuMP.value(t.var[next[1]]), t), next[2] -end -function solution_table( - var::Containers.SparseAxisArray{T,N,K}, - name, - colnames..., -) where {T<:AbstractVariableRef,N,K} - return _SolutionTableSparse(var, name, colnames...) + C = (col_names..., name) + return vec([NamedTuple{C}((args..., f(x[i]))) for (i, args) in _row_iterator(x)]) end diff --git a/test/tables.jl b/test/tables.jl index 5993a8b6519..524692d0ad7 100644 --- a/test/tables.jl +++ b/test/tables.jl @@ -1,7 +1,6 @@ module TestTableInterface using JuMP -using Tables using Test function runtests() @@ -18,6 +17,8 @@ end function test_denseaxisarray() model = Model() @variable(model, x[i = 4:10, j = 2002:2022] >= 0) + @test typeof(x) <: Containers.DenseAxisArray + @objective(model, Min, sum(x)) set_optimizer( model, @@ -42,19 +43,20 @@ function test_denseaxisarray() ) end - tbl = JuMP.solution_table(x, :solution, :index1, :index2) - @test Tables.istable(typeof(tbl)) - @test Tables.rowaccess(typeof(tbl)) - + tbl = JuMP.table(value, x, :solution, :index1, :index2) + tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) - @test Tables.getcolumn(tblrow, :index1) == 4 - @test Tables.getcolumn(tblrow, 1) == 4 + @test tblrow.solution == 0 @test tblrow.index1 == 4 - @test propertynames(tblrow) == [:index1, :index2, :solution] + @test propertynames(tblrow) == (:index1, :index2, :solution) rows = collect(tbl) @test length(rows) == length(tbl) + + var_tbl = JuMP.table(x, :variable, :index1, :index2) + @test typeof(first(var_tbl).variable) <: VariableRef + end function test_array() @@ -86,17 +88,13 @@ function test_array() ) end - tbl = JuMP.solution_table(x, :solution, :index1, :index2) - - @test Tables.istable(typeof(tbl)) - @test Tables.rowaccess(typeof(tbl)) + tbl = JuMP.table(value, x, :solution, :index1, :index2) tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) - @test Tables.getcolumn(tblrow, :index1) == 1 - @test Tables.getcolumn(tblrow, 1) == 1 + @test tblrow.solution == 0 @test tblrow.index1 == 1 - @test propertynames(tblrow) == [:index1, :index2, :solution] + @test propertynames(tblrow) == (:index1, :index2, :solution) rows = collect(tbl) @test length(rows) == length(tbl) @@ -130,16 +128,13 @@ function test_sparseaxisarray() ) end - tbl = JuMP.solution_table(x, :solution, :index1, :index2) - @test Tables.istable(typeof(tbl)) - @test Tables.rowaccess(typeof(tbl)) - + tbl = JuMP.table(value, x, :solution, :index1, :index2) + tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) - @test Tables.getcolumn(tblrow, :index1) == 1 - @test Tables.getcolumn(tblrow, 1) == 1 + @test tblrow.solution == 0 @test tblrow.index1 == 1 - @test propertynames(tblrow) == [:index1, :index2, :solution] + @test propertynames(tblrow) == (:index1, :index2, :solution) rows = collect(tbl) @test length(rows) == length(tbl) @@ -202,16 +197,13 @@ function test_custom_variable() ) end - tbl = JuMP.solution_table(x, :solution, :index1, :index2) - @test Tables.istable(typeof(tbl)) - @test Tables.rowaccess(typeof(tbl)) - + tbl = JuMP.table(value, x, :solution, :index1, :index2) + tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) - @test Tables.getcolumn(tblrow, :index1) == 1 - @test Tables.getcolumn(tblrow, 1) == 1 + @test tblrow.solution == 0 @test tblrow.index1 == 1 - @test propertynames(tblrow) == [:index1, :index2, :solution] + @test propertynames(tblrow) == (:index1, :index2, :solution) end end From 526d47e4f17d32385dcc9e60fe9950a995c6ccdf Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 17 Oct 2022 09:41:57 +1300 Subject: [PATCH 06/18] Update tables.jl --- src/tables.jl | 64 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/tables.jl b/src/tables.jl index b98695f2447..c5d7d7451b9 100644 --- a/src/tables.jl +++ b/src/tables.jl @@ -1,49 +1,61 @@ -function _row_iterator(x::Union{Array,Containers.DenseAxisArray}) +# 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/. + +function _row_iterator(x::Array) return zip(eachindex(x), Iterators.product(axes(x)...)) end +function _row_iterator(x::Containers.DenseAxisArray) + return zip(vec(eachindex(x)), Iterators.product(axes(x)...)) +end + function _row_iterator(x::Containers.SparseAxisArray) return zip(eachindex(x.data), keys(x.data)) end -_columns(x::Union{Array, Containers.DenseAxisArray}) = length(axes(x)) -_columns(x::Containers.SparseAxisArray{T,N,K}) where {T,N,K} = N +""" + table([f::Function=identity,] x, value_name::Symbol, col_names::Symbol...) +Applies the function `f` to all elements of the variable container `x`, +returning the result as a `Vector` of `NamedTuple`s, where `col_names` +are used for the correspondig axis names, and `value_name` is used for the +result of `f(x[i])`. -function table(x, name::Symbol, col_names::Symbol...) - return table(identity, x, name, col_names...) -end +!!! 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. -""" - table(f::Function, x, name, colnames...) +## Example -Applies the function `f` to all elements of the variable container `x` -and returns the result as a `Vector` of `NamedTuple`s using the provided -names. +```jldoctest; setup=:(using JuMP) +julia> model = Model(); -A `Vector` of `NamedTuple`s implements the 'Tables.jl' interface -and can be used as input for anything that consumes a 'Tables.jl' -compatible source. +julia> @variable(model, x[i=1:2, j=i:2] >= 0, start = i+j); -## Example -```julia -model = Model() -@variable(model, x[1:10, 2000:2020] >= 0) -[...] -optimize!(model) -tbl = table(value, x, :solution_value, :car, :year) +julia> table(start_value, x, :start, :I, :J) +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,Containers.DenseAxisArray,Containers.SparseAxisArray}, - name::Symbol, + value_name::Symbol, col_names::Symbol..., ) - if length(col_names) < _columns(x) - error("Not enough column names provided") + got, want = length(col_names), ndims(x) + if got != want + error("Invalid number column names provided: Got $got, expected $want.") end + C = (col_names..., value_name) + return [NamedTuple{C}((args..., f(x[i]))) for (i, args) in _row_iterator(x)] +end - C = (col_names..., name) - return vec([NamedTuple{C}((args..., f(x[i]))) for (i, args) in _row_iterator(x)]) +function table(x, value_name::Symbol, col_names::Symbol...) + return table(identity, x, value_name, col_names...) end From cfae78224abfbbbaf68d3089e5ced7e38cf001b3 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 17 Oct 2022 09:46:29 +1300 Subject: [PATCH 07/18] Update tables.jl --- test/tables.jl | 140 ++++++++----------------------------------------- 1 file changed, 22 insertions(+), 118 deletions(-) diff --git a/test/tables.jl b/test/tables.jl index 524692d0ad7..29a17e88f38 100644 --- a/test/tables.jl +++ b/test/tables.jl @@ -1,3 +1,8 @@ +# 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 @@ -16,128 +21,49 @@ end function test_denseaxisarray() model = Model() - @variable(model, x[i = 4:10, j = 2002:2022] >= 0) + @variable(model, x[i = 4:10, j = 2002:2022] >= 0, start = 0.0) @test typeof(x) <: Containers.DenseAxisArray - - @objective(model, Min, sum(x)) - set_optimizer( - model, - () -> MOI.Utilities.MockOptimizer( - MOI.Utilities.Model{Float64}(); - eval_objective_value = false, - ), - ) - optimize!(model) - mockoptimizer = JuMP.unsafe_backend(model) - MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) - MOI.set(mockoptimizer, MOI.ResultCount(), 1) - MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) - MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - - for ind in eachindex(x) - MOI.set( - mockoptimizer, - MOI.VariablePrimal(), - JuMP.optimizer_index(x[ind]), - 0.0, - ) - end - - tbl = JuMP.table(value, x, :solution, :index1, :index2) - + tbl = JuMP.table(start_value, x, :solution, :index1, :index2) tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) @test tblrow.solution == 0 @test tblrow.index1 == 4 @test propertynames(tblrow) == (:index1, :index2, :solution) - rows = collect(tbl) @test length(rows) == length(tbl) - var_tbl = JuMP.table(x, :variable, :index1, :index2) @test typeof(first(var_tbl).variable) <: VariableRef - + return end function test_array() model = Model() - @variable(model, x[1:10, 1:5] >= 0) + @variable(model, x[1:10, 1:5] >= 0, start = 0.0) @test typeof(x) <: Array{VariableRef} - - @objective(model, Min, sum(x)) - set_optimizer( - model, - () -> MOI.Utilities.MockOptimizer( - MOI.Utilities.Model{Float64}(); - eval_objective_value = false, - ), - ) - optimize!(model) - mockoptimizer = JuMP.unsafe_backend(model) - MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) - MOI.set(mockoptimizer, MOI.ResultCount(), 1) - MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) - MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - - for ind in eachindex(x) - MOI.set( - mockoptimizer, - MOI.VariablePrimal(), - JuMP.optimizer_index(x[ind]), - 0.0, - ) - end - - tbl = JuMP.table(value, x, :solution, :index1, :index2) - + tbl = JuMP.table(start_value, x, :solution, :index1, :index2) tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) @test tblrow.solution == 0 @test tblrow.index1 == 1 @test propertynames(tblrow) == (:index1, :index2, :solution) - rows = collect(tbl) @test length(rows) == length(tbl) + return end function test_sparseaxisarray() model = Model() - @variable(model, x[i = 1:10, j = 1:5; i + j <= 8] >= 0) + @variable(model, x[i = 1:10, j = 1:5; i + j <= 8] >= 0, start = 0) @test typeof(x) <: Containers.SparseAxisArray - @objective(model, Min, sum(x)) - set_optimizer( - model, - () -> MOI.Utilities.MockOptimizer( - MOI.Utilities.Model{Float64}(); - eval_objective_value = false, - ), - ) - optimize!(model) - mockoptimizer = JuMP.unsafe_backend(model) - MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) - MOI.set(mockoptimizer, MOI.ResultCount(), 1) - MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) - MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - - for ind in eachindex(x) - MOI.set( - mockoptimizer, - MOI.VariablePrimal(), - JuMP.optimizer_index(x[ind]), - 0.0, - ) - end - - tbl = JuMP.table(value, x, :solution, :index1, :index2) - + tbl = JuMP.table(start_value, x, :solution, :index1, :index2) tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) - @test tblrow.solution == 0 + @test tblrow.solution == 0.0 @test tblrow.index1 == 1 @test propertynames(tblrow) == (:index1, :index2, :solution) - rows = collect(tbl) @test length(rows) == length(tbl) + return end # Mockup of custom variable type @@ -150,7 +76,9 @@ struct _MockVariableRef <: JuMP.AbstractVariableRef end JuMP.name(v::_MockVariableRef) = JuMP.name(v.vref) + JuMP.owner_model(v::_MockVariableRef) = JuMP.owner_model(v.vref) + JuMP.value(v::_MockVariableRef) = JuMP.value(v.vref) struct _Mock end @@ -170,40 +98,16 @@ function test_custom_variable() model, x[i = 1:3, j = 100:102] >= 0, _Mock(), - container = Containers.DenseAxisArray - ) - - @objective(model, Min, 0) - set_optimizer( - model, - () -> MOI.Utilities.MockOptimizer( - MOI.Utilities.Model{Float64}(); - eval_objective_value = false, - ), + container = Containers.DenseAxisArray, + start = 0.0, ) - optimize!(model) - mockoptimizer = JuMP.unsafe_backend(model) - MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) - MOI.set(mockoptimizer, MOI.ResultCount(), 1) - MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) - MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) - - for ind in eachindex(x) - MOI.set( - mockoptimizer, - MOI.VariablePrimal(), - JuMP.optimizer_index(x[ind].vref), - 0.0, - ) - end - - tbl = JuMP.table(value, x, :solution, :index1, :index2) - + tbl = JuMP.table(start_value, x, :solution, :index1, :index2) tblrow = first(tbl) @test eltype(tbl) == typeof(tblrow) - @test tblrow.solution == 0 + @test tblrow.solution == 0.0 @test tblrow.index1 == 1 @test propertynames(tblrow) == (:index1, :index2, :solution) + return end end From b7d63e6826b64f2f74a4c061bd869916679886f0 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 17 Oct 2022 09:52:53 +1300 Subject: [PATCH 08/18] Fix tests --- src/tables.jl | 8 +++--- test/tables.jl | 71 ++++++++++++++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/tables.jl b/src/tables.jl index c5d7d7451b9..3cf3766516d 100644 --- a/src/tables.jl +++ b/src/tables.jl @@ -26,7 +26,7 @@ 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. + that consumes a 'Tables.jl' compatible source. ## Example @@ -43,9 +43,9 @@ julia> table(start_value, x, :start, :I, :J) ``` """ function table( - f::Function, - x::Union{Array,Containers.DenseAxisArray,Containers.SparseAxisArray}, - value_name::Symbol, + f::Function, + x::Union{Array,Containers.DenseAxisArray,Containers.SparseAxisArray}, + value_name::Symbol, col_names::Symbol..., ) got, want = length(col_names), ndims(x) diff --git a/test/tables.jl b/test/tables.jl index 29a17e88f38..c0b5a57e330 100644 --- a/test/tables.jl +++ b/test/tables.jl @@ -23,14 +23,14 @@ function test_denseaxisarray() model = Model() @variable(model, x[i = 4:10, j = 2002:2022] >= 0, start = 0.0) @test typeof(x) <: Containers.DenseAxisArray - tbl = JuMP.table(start_value, x, :solution, :index1, :index2) - tblrow = first(tbl) - @test eltype(tbl) == typeof(tblrow) - @test tblrow.solution == 0 - @test tblrow.index1 == 4 - @test propertynames(tblrow) == (:index1, :index2, :solution) - rows = collect(tbl) - @test length(rows) == length(tbl) + start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + row = first(start_table) + @test eltype(start_table) == typeof(row) + @test row.solution == 0 + @test row.index1 == 4 + @test propertynames(row) == (:index1, :index2, :solution) + rows = collect(start_table) + @test length(rows) == length(start_table) var_tbl = JuMP.table(x, :variable, :index1, :index2) @test typeof(first(var_tbl).variable) <: VariableRef return @@ -40,14 +40,14 @@ function test_array() model = Model() @variable(model, x[1:10, 1:5] >= 0, start = 0.0) @test typeof(x) <: Array{VariableRef} - tbl = JuMP.table(start_value, x, :solution, :index1, :index2) - tblrow = first(tbl) - @test eltype(tbl) == typeof(tblrow) - @test tblrow.solution == 0 - @test tblrow.index1 == 1 - @test propertynames(tblrow) == (:index1, :index2, :solution) - rows = collect(tbl) - @test length(rows) == length(tbl) + start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + row = first(start_table) + @test eltype(start_table) == typeof(row) + @test row.solution == 0 + @test row.index1 == 1 + @test propertynames(row) == (:index1, :index2, :solution) + rows = collect(start_table) + @test length(rows) == length(start_table) return end @@ -55,14 +55,23 @@ 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 - tbl = JuMP.table(start_value, x, :solution, :index1, :index2) - tblrow = first(tbl) - @test eltype(tbl) == typeof(tblrow) - @test tblrow.solution == 0.0 - @test tblrow.index1 == 1 - @test propertynames(tblrow) == (:index1, :index2, :solution) - rows = collect(tbl) - @test length(rows) == length(tbl) + start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + row = first(start_table) + @test eltype(start_table) == typeof(row) + @test row.solution == 0.0 + @test row.index1 == 1 + @test propertynames(row) == (:index1, :index2, :solution) + rows = collect(start_table) + @test length(rows) == length(start_table) + return +end + +function test_col_name_error() + model = Model() + @variable(model, x[1:2, 1:2]) + @test_throws ErrorException table(x, :y, :a) + @test_throws ErrorException table(x, :y, :a, :b, :c) + @test table(x, :y, :a, :b) isa Vector{<:NamedTuple} return end @@ -79,7 +88,7 @@ JuMP.name(v::_MockVariableRef) = JuMP.name(v.vref) JuMP.owner_model(v::_MockVariableRef) = JuMP.owner_model(v.vref) -JuMP.value(v::_MockVariableRef) = JuMP.value(v.vref) +JuMP.start_value(v::_MockVariableRef) = JuMP.start_value(v.vref) struct _Mock end @@ -101,12 +110,12 @@ function test_custom_variable() container = Containers.DenseAxisArray, start = 0.0, ) - tbl = JuMP.table(start_value, x, :solution, :index1, :index2) - tblrow = first(tbl) - @test eltype(tbl) == typeof(tblrow) - @test tblrow.solution == 0.0 - @test tblrow.index1 == 1 - @test propertynames(tblrow) == (:index1, :index2, :solution) + start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + row = first(start_table) + @test eltype(start_table) == typeof(row) + @test row.solution == 0.0 + @test row.index1 == 1 + @test propertynames(row) == (:index1, :index2, :solution) return end From 9bf2cf8208bebe781de27815f530322b49d95df8 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 17 Oct 2022 10:07:58 +1300 Subject: [PATCH 09/18] Make tests deterministic --- test/tables.jl | 51 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/test/tables.jl b/test/tables.jl index c0b5a57e330..98f929c841d 100644 --- a/test/tables.jl +++ b/test/tables.jl @@ -24,15 +24,13 @@ function test_denseaxisarray() @variable(model, x[i = 4:10, j = 2002:2022] >= 0, start = 0.0) @test typeof(x) <: Containers.DenseAxisArray start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + 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 eltype(start_table) == typeof(row) - @test row.solution == 0 - @test row.index1 == 4 - @test propertynames(row) == (:index1, :index2, :solution) - rows = collect(start_table) - @test length(rows) == length(start_table) - var_tbl = JuMP.table(x, :variable, :index1, :index2) - @test typeof(first(var_tbl).variable) <: VariableRef + @test row == (index1 = 4, index2 = 2002, solution = 0.0) + x_table = JuMP.table(x, :variable, :index1, :index2) + @test x_table[1] == (index1 = 4, index2 = 2002, variable = x[4, 2002]) return end @@ -41,13 +39,13 @@ function test_array() @variable(model, x[1:10, 1:5] >= 0, start = 0.0) @test typeof(x) <: Array{VariableRef} start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + 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 eltype(start_table) == typeof(row) - @test row.solution == 0 - @test row.index1 == 1 - @test propertynames(row) == (:index1, :index2, :solution) - rows = collect(start_table) - @test length(rows) == length(start_table) + @test row == (index1 = 1, index2 = 1, solution = 0.0) + x_table = JuMP.table(x, :variable, :index1, :index2) + @test x_table[1] == (index1 = 1, index2 = 1, variable = x[1, 1]) return end @@ -56,13 +54,12 @@ function test_sparseaxisarray() @variable(model, x[i = 1:10, j = 1:5; i + j <= 8] >= 0, start = 0) @test typeof(x) <: Containers.SparseAxisArray start_table = JuMP.table(start_value, x, :solution, :index1, :index2) - row = first(start_table) - @test eltype(start_table) == typeof(row) - @test row.solution == 0.0 - @test row.index1 == 1 - @test propertynames(row) == (:index1, :index2, :solution) - rows = collect(start_table) - @test length(rows) == length(start_table) + 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 = JuMP.table(x, :variable, :index1, :index2) + @test (index1 = 1, index2 = 1, variable = x[1, 1]) in x_table return end @@ -110,12 +107,14 @@ function test_custom_variable() container = Containers.DenseAxisArray, start = 0.0, ) + @test typeof(x) <: Containers.DenseAxisArray start_table = JuMP.table(start_value, x, :solution, :index1, :index2) - row = first(start_table) - @test eltype(start_table) == typeof(row) - @test row.solution == 0.0 - @test row.index1 == 1 - @test propertynames(row) == (: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 = JuMP.table(x, :variable, :index1, :index2) + @test (index1 = 1, index2 = 100, variable = x[1, 100]) in x_table return end From f82a06254752ea0c27c72e1ebb72e4853d73ceaf Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 17 Oct 2022 11:37:33 +1300 Subject: [PATCH 10/18] Move to containers and add docs --- docs/src/manual/containers.md | 100 ++++++++++++++++++++++++++++++ docs/src/reference/containers.md | 1 + docs/src/tutorials/linear/diet.jl | 10 +++ src/Containers/Containers.jl | 1 + src/{ => Containers}/tables.jl | 8 +-- src/JuMP.jl | 1 - test/{ => Containers}/tables.jl | 16 ++--- 7 files changed, 124 insertions(+), 13 deletions(-) rename src/{ => Containers}/tables.jl (89%) rename test/{ => Containers}/tables.jl (86%) diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index ce47643cc0c..2b7c16bc92c 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -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, :value, :I, :J) +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 @@ -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, :value, :I, :J) +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 @@ -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_dense +julia> table = Containers.table(x, :value, :I, :J) +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_dense +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: diff --git a/docs/src/reference/containers.md b/docs/src/reference/containers.md index e2679c6f9d4..6e80a5e3bff 100644 --- a/docs/src/reference/containers.md +++ b/docs/src/reference/containers.md @@ -7,6 +7,7 @@ Containers Containers.DenseAxisArray Containers.SparseAxisArray Containers.container +Containers.table Containers.default_container Containers.@container Containers.VectorizedProductIterator diff --git a/docs/src/tutorials/linear/diet.jl b/docs/src/tutorials/linear/diet.jl index 80f07a1ba01..4a86959da0e 100644 --- a/docs/src/tutorials/linear/diet.jl +++ b/docs/src/tutorials/linear/diet.jl @@ -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, :quantity, :food) +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 diff --git a/src/Containers/Containers.jl b/src/Containers/Containers.jl index d42ceacb223..dbc1aa74f3e 100644 --- a/src/Containers/Containers.jl +++ b/src/Containers/Containers.jl @@ -54,5 +54,6 @@ include("nested_iterator.jl") include("no_duplicate_dict.jl") include("container.jl") include("macro.jl") +include("tables.jl") end diff --git a/src/tables.jl b/src/Containers/tables.jl similarity index 89% rename from src/tables.jl rename to src/Containers/tables.jl index 3cf3766516d..576cf01e78e 100644 --- a/src/tables.jl +++ b/src/Containers/tables.jl @@ -7,11 +7,11 @@ function _row_iterator(x::Array) return zip(eachindex(x), Iterators.product(axes(x)...)) end -function _row_iterator(x::Containers.DenseAxisArray) +function _row_iterator(x::DenseAxisArray) return zip(vec(eachindex(x)), Iterators.product(axes(x)...)) end -function _row_iterator(x::Containers.SparseAxisArray) +function _row_iterator(x::SparseAxisArray) return zip(eachindex(x.data), keys(x.data)) end @@ -35,7 +35,7 @@ julia> model = Model(); julia> @variable(model, x[i=1:2, j=i:2] >= 0, start = i+j); -julia> table(start_value, x, :start, :I, :J) +julia> Containers.table(start_value, x, :start, :I, :J) 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) @@ -44,7 +44,7 @@ julia> table(start_value, x, :start, :I, :J) """ function table( f::Function, - x::Union{Array,Containers.DenseAxisArray,Containers.SparseAxisArray}, + x::Union{Array,DenseAxisArray,SparseAxisArray}, value_name::Symbol, col_names::Symbol..., ) diff --git a/src/JuMP.jl b/src/JuMP.jl index 7dce4e1066f..7252fe83554 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1359,7 +1359,6 @@ include("lp_sensitivity2.jl") include("callbacks.jl") include("file_formats.jl") include("feasibility_checker.jl") -include("tables.jl") # MOI contains a number of Enums that are often accessed by users such as # `MOI.OPTIMAL`. This piece of code re-exports them from JuMP so that users can diff --git a/test/tables.jl b/test/Containers/tables.jl similarity index 86% rename from test/tables.jl rename to test/Containers/tables.jl index 98f929c841d..bcc6310b448 100644 --- a/test/tables.jl +++ b/test/Containers/tables.jl @@ -23,13 +23,13 @@ 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 = JuMP.table(start_value, x, :solution, :index1, :index2) + start_table = Containers.table(start_value, x, :solution, :index1, :index2) 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 = JuMP.table(x, :variable, :index1, :index2) + x_table = Containers.table(x, :variable, :index1, :index2) @test x_table[1] == (index1 = 4, index2 = 2002, variable = x[4, 2002]) return end @@ -38,13 +38,13 @@ function test_array() model = Model() @variable(model, x[1:10, 1:5] >= 0, start = 0.0) @test typeof(x) <: Array{VariableRef} - start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + start_table = Containers.table(start_value, x, :solution, :index1, :index2) 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 = JuMP.table(x, :variable, :index1, :index2) + x_table = Containers.table(x, :variable, :index1, :index2) @test x_table[1] == (index1 = 1, index2 = 1, variable = x[1, 1]) return end @@ -53,12 +53,12 @@ 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 = JuMP.table(start_value, x, :solution, :index1, :index2) + start_table = Containers.table(start_value, x, :solution, :index1, :index2) 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 = JuMP.table(x, :variable, :index1, :index2) + x_table = Containers.table(x, :variable, :index1, :index2) @test (index1 = 1, index2 = 1, variable = x[1, 1]) in x_table return end @@ -108,12 +108,12 @@ function test_custom_variable() start = 0.0, ) @test typeof(x) <: Containers.DenseAxisArray - start_table = JuMP.table(start_value, x, :solution, :index1, :index2) + start_table = Containers.table(start_value, x, :solution, :index1, :index2) 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 = JuMP.table(x, :variable, :index1, :index2) + x_table = Containers.table(x, :variable, :index1, :index2) @test (index1 = 1, index2 = 100, variable = x[1, 100]) in x_table return end From e3edcb28ab2440cc8e9eee8c2042b16ef2df3404 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 17 Oct 2022 11:45:00 +1300 Subject: [PATCH 11/18] Change name order --- docs/src/manual/containers.md | 6 ++--- docs/src/tutorials/linear/diet.jl | 2 +- src/Containers/tables.jl | 42 ++++++++++++------------------- test/Containers/tables.jl | 22 ++++++++-------- 4 files changed, 31 insertions(+), 41 deletions(-) diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index 2b7c16bc92c..53798bbbd0a 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -109,7 +109,7 @@ Use [`Containers.table`](@ref) to convert the `Array` into a `Vector{<:NamedTuple}`: ```jldoctest containers_array -julia> table = Containers.table(x, :value, :I, :J) +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)) @@ -234,7 +234,7 @@ Use [`Containers.table`](@ref) to convert the `DenseAxisArray` into a `Vector{<:NamedTuple}`: ```jldoctest containers_dense -julia> table = Containers.table(x, :value, :I, :J) +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)) @@ -327,7 +327,7 @@ Use [`Containers.table`](@ref) to convert the `SparseAxisArray` into a `Vector{<:NamedTuple}`: ```jldoctest containers_dense -julia> table = Containers.table(x, :value, :I, :J) +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)) diff --git a/docs/src/tutorials/linear/diet.jl b/docs/src/tutorials/linear/diet.jl index 4a86959da0e..1cf44fc7d0b 100644 --- a/docs/src/tutorials/linear/diet.jl +++ b/docs/src/tutorials/linear/diet.jl @@ -142,7 +142,7 @@ end # We can also use the function [`Containers.table`](@ref) to easily convert the # result into a DataFrame: -table = Containers.table(value, x, :quantity, :food) +table = Containers.table(value, x, :food, :quantity) solution = DataFrames.DataFrame(table) # This makes it easy to perform analyses our solution: diff --git a/src/Containers/tables.jl b/src/Containers/tables.jl index 576cf01e78e..bc296f72f09 100644 --- a/src/Containers/tables.jl +++ b/src/Containers/tables.jl @@ -3,25 +3,19 @@ # 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/. -function _row_iterator(x::Array) - return zip(eachindex(x), Iterators.product(axes(x)...)) -end +_rows(x::Array) = zip(eachindex(x), Iterators.product(axes(x)...)) -function _row_iterator(x::DenseAxisArray) - return zip(vec(eachindex(x)), Iterators.product(axes(x)...)) -end +_rows(x::DenseAxisArray) = zip(vec(eachindex(x)), Iterators.product(axes(x)...)) -function _row_iterator(x::SparseAxisArray) - return zip(eachindex(x.data), keys(x.data)) -end +_rows(x::SparseAxisArray) = zip(eachindex(x.data), keys(x.data)) """ - table([f::Function=identity,] x, value_name::Symbol, col_names::Symbol...) + 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 `col_names` -are used for the correspondig axis names, and `value_name` is used for the -result of `f(x[i])`. +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) @@ -35,27 +29,23 @@ julia> model = Model(); julia> @variable(model, x[i=1:2, j=i:2] >= 0, start = i+j); -julia> Containers.table(start_value, x, :start, :I, :J) -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) +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}, - value_name::Symbol, - col_names::Symbol..., + names::Symbol..., ) - got, want = length(col_names), ndims(x) + got, want = length(names), ndims(x) + 1 if got != want error("Invalid number column names provided: Got $got, expected $want.") end - C = (col_names..., value_name) - return [NamedTuple{C}((args..., f(x[i]))) for (i, args) in _row_iterator(x)] + return [NamedTuple{names}((args..., f(x[i]))) for (i, args) in _rows(x)] end -function table(x, value_name::Symbol, col_names::Symbol...) - return table(identity, x, value_name, col_names...) -end +table(x, names::Symbol...) = table(identity, x, names...) diff --git a/test/Containers/tables.jl b/test/Containers/tables.jl index bcc6310b448..80e38f40f02 100644 --- a/test/Containers/tables.jl +++ b/test/Containers/tables.jl @@ -23,13 +23,13 @@ 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, :solution, :index1, :index2) + 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, :variable, :index1, :index2) + x_table = Containers.table(x, :index1, :index2, :variable) @test x_table[1] == (index1 = 4, index2 = 2002, variable = x[4, 2002]) return end @@ -38,13 +38,13 @@ 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, :solution, :index1, :index2) + 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, :variable, :index1, :index2) + x_table = Containers.table(x, :index1, :index2, :variable) @test x_table[1] == (index1 = 1, index2 = 1, variable = x[1, 1]) return end @@ -53,12 +53,12 @@ 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, :solution, :index1, :index2) + 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, :variable, :index1, :index2) + x_table = Containers.table(x, :index1, :index2, :variable) @test (index1 = 1, index2 = 1, variable = x[1, 1]) in x_table return end @@ -66,9 +66,9 @@ end function test_col_name_error() model = Model() @variable(model, x[1:2, 1:2]) - @test_throws ErrorException table(x, :y, :a) - @test_throws ErrorException table(x, :y, :a, :b, :c) - @test table(x, :y, :a, :b) isa Vector{<:NamedTuple} + @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 @@ -108,12 +108,12 @@ function test_custom_variable() start = 0.0, ) @test typeof(x) <: Containers.DenseAxisArray - start_table = Containers.table(start_value, x, :solution, :index1, :index2) + 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, :variable, :index1, :index2) + x_table = Containers.table(x, :index1, :index2, :variable) @test (index1 = 1, index2 = 100, variable = x[1, 100]) in x_table return end From 15c6c8c87920d0194271af5ed834dd45452bc3ae Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 17 Oct 2022 11:57:55 +1300 Subject: [PATCH 12/18] Update containers.md --- docs/src/manual/containers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index 53798bbbd0a..3f63f6d8419 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -326,7 +326,7 @@ Use [`Containers.table`](@ref) to convert the `SparseAxisArray` into a [Tables.jl](https://github.com/JuliaData/Tables.jl) compatible `Vector{<:NamedTuple}`: -```jldoctest containers_dense +```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)) @@ -338,7 +338,7 @@ julia> table = Containers.table(x, :I, :J, :value) 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 +```jldoctest containers_sparse julia> import DataFrames; julia> DataFrames.DataFrame(table) From 228e89feaeb45c172b0ef7ee527115e7161ebd50 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 17 Oct 2022 12:41:14 +1300 Subject: [PATCH 13/18] Update src/Containers/tables.jl --- src/Containers/tables.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Containers/tables.jl b/src/Containers/tables.jl index bc296f72f09..59cd2749b1a 100644 --- a/src/Containers/tables.jl +++ b/src/Containers/tables.jl @@ -43,7 +43,7 @@ function table( ) got, want = length(names), ndims(x) + 1 if got != want - error("Invalid number column names provided: Got $got, expected $want.") + error("Invalid number of column names provided: Got $got, expected $want.") end return [NamedTuple{names}((args..., f(x[i]))) for (i, args) in _rows(x)] end From 8fc96ada37c8abb5a947d06b2bed1bcbe272b314 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 17 Oct 2022 12:45:22 +1300 Subject: [PATCH 14/18] Update src/Containers/tables.jl --- src/Containers/tables.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Containers/tables.jl b/src/Containers/tables.jl index 59cd2749b1a..38ec33b5acf 100644 --- a/src/Containers/tables.jl +++ b/src/Containers/tables.jl @@ -43,7 +43,9 @@ function table( ) got, want = length(names), ndims(x) + 1 if got != want - error("Invalid number of column names provided: Got $got, expected $want.") + error( + "Invalid number of column names provided: Got $got, expected $want.", + ) end return [NamedTuple{names}((args..., f(x[i]))) for (i, args) in _rows(x)] end From 064d8553e4abc088b9c50b12dd01909b7ecdf571 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 18 Oct 2022 11:07:48 +1300 Subject: [PATCH 15/18] Support default names --- src/Containers/tables.jl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Containers/tables.jl b/src/Containers/tables.jl index 38ec33b5acf..d3e352386cf 100644 --- a/src/Containers/tables.jl +++ b/src/Containers/tables.jl @@ -10,13 +10,15 @@ _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...) + 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])`. +If `names` are omitted, then the default names are `(:x1, :x2, ..., :xN, :y)`. + !!! 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 @@ -34,6 +36,12 @@ julia> Containers.table(start_value, x, :i, :j, :start) (i = 1, j = 2, start = 3.0) (i = 1, j = 1, start = 2.0) (i = 2, j = 2, start = 4.0) + +julia> Containers.table(x) +3-element Vector{NamedTuple{(:x1, :x2, :y), Tuple{Int64, Int64, VariableRef}}}: + (x1 = 1, x2 = 2, y = x[1,2]) + (x1 = 1, x2 = 1, y = x[1,1]) + (x1 = 2, x2 = 2, y = x[2,2]) ``` """ function table( @@ -41,6 +49,9 @@ function table( x::Union{Array,DenseAxisArray,SparseAxisArray}, names::Symbol..., ) + if length(names) == 0 + return table(f, x, [Symbol("x$i") for i in 1:ndims(x)]..., :y) + end got, want = length(names), ndims(x) + 1 if got != want error( From 5a372bce05830c23a2bee61a9a430bfbebde98d4 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 18 Oct 2022 12:44:30 +1300 Subject: [PATCH 16/18] Update tables.jl --- test/Containers/tables.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Containers/tables.jl b/test/Containers/tables.jl index 80e38f40f02..8e9b54fdd64 100644 --- a/test/Containers/tables.jl +++ b/test/Containers/tables.jl @@ -108,13 +108,13 @@ function test_custom_variable() 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}} + start_table = Containers.table(start_value, x) + T = NamedTuple{(:x1, :x2, :y),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 + @test (x1 = 1, x2 = 100, y = 0.0) in start_table + x_table = Containers.table(x) + @test (x1 = 1, x2 = 100, y = x[1, 100]) in x_table return end From 132c1104c4948ed4ad73e2c95bb9ef5d11aedd9e Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 18 Oct 2022 13:25:40 +1300 Subject: [PATCH 17/18] s/table/rowtable --- docs/src/manual/containers.md | 12 +++++----- docs/src/reference/containers.md | 2 +- docs/src/tutorials/linear/diet.jl | 6 ++--- src/Containers/tables.jl | 12 +++++----- test/Containers/tables.jl | 40 +++++++++++++++---------------- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index 3f63f6d8419..07c960ee11c 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -104,12 +104,12 @@ julia> swap.(x) ### Tables -Use [`Containers.table`](@ref) to convert the `Array` into a +Use [`Containers.rowtable`](@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) +julia> table = Containers.rowtable(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)) @@ -229,12 +229,12 @@ julia> x.data ### Tables -Use [`Containers.table`](@ref) to convert the `DenseAxisArray` into a +Use [`Containers.rowtable`](@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) +julia> table = Containers.rowtable(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)) @@ -322,12 +322,12 @@ JuMP.Containers.SparseAxisArray{Tuple{Symbol, Int64}, 1, Tuple{Int64}} with 2 en ### Tables -Use [`Containers.table`](@ref) to convert the `SparseAxisArray` into a +Use [`Containers.rowtable`](@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) +julia> table = Containers.rowtable(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)) diff --git a/docs/src/reference/containers.md b/docs/src/reference/containers.md index 6e80a5e3bff..12c7bc16142 100644 --- a/docs/src/reference/containers.md +++ b/docs/src/reference/containers.md @@ -7,7 +7,7 @@ Containers Containers.DenseAxisArray Containers.SparseAxisArray Containers.container -Containers.table +Containers.rowtable Containers.default_container Containers.@container Containers.VectorizedProductIterator diff --git a/docs/src/tutorials/linear/diet.jl b/docs/src/tutorials/linear/diet.jl index 1cf44fc7d0b..99e6065046a 100644 --- a/docs/src/tutorials/linear/diet.jl +++ b/docs/src/tutorials/linear/diet.jl @@ -139,10 +139,10 @@ 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: +# We can also use the function [`Containers.rowtable`](@ref) to easily convert +# the result into a DataFrame: -table = Containers.table(value, x, :food, :quantity) +table = Containers.rowtable(value, x, :food, :quantity) solution = DataFrames.DataFrame(table) # This makes it easy to perform analyses our solution: diff --git a/src/Containers/tables.jl b/src/Containers/tables.jl index d3e352386cf..5f2f2729ad0 100644 --- a/src/Containers/tables.jl +++ b/src/Containers/tables.jl @@ -10,7 +10,7 @@ _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...]) + rowtable([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 @@ -31,26 +31,26 @@ 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) +julia> Containers.rowtable(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) -julia> Containers.table(x) +julia> Containers.rowtable(x) 3-element Vector{NamedTuple{(:x1, :x2, :y), Tuple{Int64, Int64, VariableRef}}}: (x1 = 1, x2 = 2, y = x[1,2]) (x1 = 1, x2 = 1, y = x[1,1]) (x1 = 2, x2 = 2, y = x[2,2]) ``` """ -function table( +function rowtable( f::Function, x::Union{Array,DenseAxisArray,SparseAxisArray}, names::Symbol..., ) if length(names) == 0 - return table(f, x, [Symbol("x$i") for i in 1:ndims(x)]..., :y) + return rowtable(f, x, [Symbol("x$i") for i in 1:ndims(x)]..., :y) end got, want = length(names), ndims(x) + 1 if got != want @@ -61,4 +61,4 @@ function table( return [NamedTuple{names}((args..., f(x[i]))) for (i, args) in _rows(x)] end -table(x, names::Symbol...) = table(identity, x, names...) +rowtable(x, names::Symbol...) = rowtable(identity, x, names...) diff --git a/test/Containers/tables.jl b/test/Containers/tables.jl index 8e9b54fdd64..439b8bc6b03 100644 --- a/test/Containers/tables.jl +++ b/test/Containers/tables.jl @@ -23,14 +23,14 @@ 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}} + start_table = Containers.rowtable(start_value, x, :i1, :i2, :solution) + T = NamedTuple{(:i1, :i2, :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]) + @test row == (i1 = 4, i2 = 2002, solution = 0.0) + x_table = Containers.rowtable(x, :i1, :i2, :variable) + @test x_table[1] == (i1 = 4, i2 = 2002, variable = x[4, 2002]) return end @@ -38,14 +38,14 @@ 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}} + start_table = Containers.rowtable(start_value, x, :i1, :i2, :solution) + T = NamedTuple{(:i1, :i2, :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]) + @test row == (i1 = 1, i2 = 1, solution = 0.0) + x_table = Containers.rowtable(x, :i1, :i2, :variable) + @test x_table[1] == (i1 = 1, i2 = 1, variable = x[1, 1]) return end @@ -53,22 +53,22 @@ 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}} + start_table = Containers.rowtable(start_value, x, :i1, :i2, :solution) + T = NamedTuple{(:i1, :i2, :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 + @test (i1 = 1, i2 = 1, solution = 0.0) in start_table + x_table = Containers.rowtable(x, :i1, :i2, :variable) + @test (i1 = 1, i2 = 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} + @test_throws ErrorException Containers.rowtable(x, :y, :a) + @test_throws ErrorException Containers.rowtable(x, :y, :a, :b, :c) + @test Containers.rowtable(x, :y, :a, :b) isa Vector{<:NamedTuple} return end @@ -108,12 +108,12 @@ function test_custom_variable() start = 0.0, ) @test typeof(x) <: Containers.DenseAxisArray - start_table = Containers.table(start_value, x) + start_table = Containers.rowtable(start_value, x) T = NamedTuple{(:x1, :x2, :y),Tuple{Int,Int,Float64}} @test start_table isa Vector{T} @test length(start_table) == length(x) @test (x1 = 1, x2 = 100, y = 0.0) in start_table - x_table = Containers.table(x) + x_table = Containers.rowtable(x) @test (x1 = 1, x2 = 100, y = x[1, 100]) in x_table return end From 139ab15728b03b4c14dbc7bf44871b7394cadc5b Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 18 Oct 2022 13:43:35 +1300 Subject: [PATCH 18/18] Switch to header::Vector{Symbol} for names --- docs/src/manual/containers.md | 6 ++--- docs/src/tutorials/linear/diet.jl | 2 +- src/Containers/tables.jl | 28 ++++++++++++---------- test/Containers/tables.jl | 39 +++++++++++++++++-------------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index 07c960ee11c..40c144572d8 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -109,7 +109,7 @@ Use [`Containers.rowtable`](@ref) to convert the `Array` into a `Vector{<:NamedTuple}`: ```jldoctest containers_array -julia> table = Containers.rowtable(x, :I, :J, :value) +julia> table = Containers.rowtable(x; header = [: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)) @@ -234,7 +234,7 @@ Use [`Containers.rowtable`](@ref) to convert the `DenseAxisArray` into a `Vector{<:NamedTuple}`: ```jldoctest containers_dense -julia> table = Containers.rowtable(x, :I, :J, :value) +julia> table = Containers.rowtable(x; header = [: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)) @@ -327,7 +327,7 @@ Use [`Containers.rowtable`](@ref) to convert the `SparseAxisArray` into a `Vector{<:NamedTuple}`: ```jldoctest containers_sparse -julia> table = Containers.rowtable(x, :I, :J, :value) +julia> table = Containers.rowtable(x; header = [: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)) diff --git a/docs/src/tutorials/linear/diet.jl b/docs/src/tutorials/linear/diet.jl index 99e6065046a..8b876b6bb50 100644 --- a/docs/src/tutorials/linear/diet.jl +++ b/docs/src/tutorials/linear/diet.jl @@ -142,7 +142,7 @@ end # We can also use the function [`Containers.rowtable`](@ref) to easily convert # the result into a DataFrame: -table = Containers.rowtable(value, x, :food, :quantity) +table = Containers.rowtable(value, x; header = [:food, :quantity]) solution = DataFrames.DataFrame(table) # This makes it easy to perform analyses our solution: diff --git a/src/Containers/tables.jl b/src/Containers/tables.jl index 5f2f2729ad0..c8b0ad536e0 100644 --- a/src/Containers/tables.jl +++ b/src/Containers/tables.jl @@ -10,14 +10,16 @@ _rows(x::DenseAxisArray) = zip(vec(eachindex(x)), Iterators.product(axes(x)...)) _rows(x::SparseAxisArray) = zip(eachindex(x.data), keys(x.data)) """ - rowtable([f::Function=identity,] x, [names::Symbol...]) + rowtable([f::Function=identity,] x; [header::Vector{Symbol} = 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])`. +returning the result as a `Vector` of `NamedTuple`s, where `header` is a vector +containing the corresponding axis names. -If `names` are omitted, then the default names are `(:x1, :x2, ..., :xN, :y)`. +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])`. + +If `header` is left empty, then the default header is `[:x1, :x2, ..., :xN, :y]`. !!! info A `Vector` of `NamedTuple`s implements the [Tables.jl](https://github.com/JuliaData/Tables.jl) @@ -31,7 +33,7 @@ julia> model = Model(); julia> @variable(model, x[i=1:2, j=i:2] >= 0, start = i+j); -julia> Containers.rowtable(start_value, x, :i, :j, :start) +julia> Containers.rowtable(start_value, x; header = [: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) @@ -46,19 +48,21 @@ julia> Containers.rowtable(x) """ function rowtable( f::Function, - x::Union{Array,DenseAxisArray,SparseAxisArray}, - names::Symbol..., + x::Union{Array,DenseAxisArray,SparseAxisArray}; + header::Vector{Symbol} = Symbol[], ) - if length(names) == 0 - return rowtable(f, x, [Symbol("x$i") for i in 1:ndims(x)]..., :y) + if isempty(header) + header = Symbol[Symbol("x$i") for i in 1:ndims(x)] + push!(header, :y) end - got, want = length(names), ndims(x) + 1 + got, want = length(header), ndims(x) + 1 if got != want error( "Invalid number of column names provided: Got $got, expected $want.", ) end + names = tuple(header...) return [NamedTuple{names}((args..., f(x[i]))) for (i, args) in _rows(x)] end -rowtable(x, names::Symbol...) = rowtable(identity, x, names...) +rowtable(x; kwargs...) = rowtable(identity, x; kwargs...) diff --git a/test/Containers/tables.jl b/test/Containers/tables.jl index 439b8bc6b03..98ee4a3a57d 100644 --- a/test/Containers/tables.jl +++ b/test/Containers/tables.jl @@ -23,14 +23,14 @@ 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.rowtable(start_value, x, :i1, :i2, :solution) - T = NamedTuple{(:i1, :i2, :solution),Tuple{Int,Int,Float64}} + start_table = Containers.rowtable(start_value, x; header = [:i1, :i2, :i3]) + T = NamedTuple{(:i1, :i2, :i3),Tuple{Int,Int,Float64}} @test start_table isa Vector{T} @test length(start_table) == length(x) row = first(start_table) - @test row == (i1 = 4, i2 = 2002, solution = 0.0) - x_table = Containers.rowtable(x, :i1, :i2, :variable) - @test x_table[1] == (i1 = 4, i2 = 2002, variable = x[4, 2002]) + @test row == (i1 = 4, i2 = 2002, i3 = 0.0) + x_table = Containers.rowtable(x; header = [:i1, :i2, :i3]) + @test x_table[1] == (i1 = 4, i2 = 2002, i3 = x[4, 2002]) return end @@ -38,14 +38,14 @@ function test_array() model = Model() @variable(model, x[1:10, 1:5] >= 0, start = 0.0) @test typeof(x) <: Array{VariableRef} - start_table = Containers.rowtable(start_value, x, :i1, :i2, :solution) - T = NamedTuple{(:i1, :i2, :solution),Tuple{Int,Int,Float64}} + start_table = Containers.rowtable(start_value, x; header = [:i1, :i2, :i3]) + T = NamedTuple{(:i1, :i2, :i3),Tuple{Int,Int,Float64}} @test start_table isa Vector{T} @test length(start_table) == length(x) row = first(start_table) - @test row == (i1 = 1, i2 = 1, solution = 0.0) - x_table = Containers.rowtable(x, :i1, :i2, :variable) - @test x_table[1] == (i1 = 1, i2 = 1, variable = x[1, 1]) + @test row == (i1 = 1, i2 = 1, i3 = 0.0) + x_table = Containers.rowtable(x; header = [:i1, :i2, :i3]) + @test x_table[1] == (i1 = 1, i2 = 1, i3 = x[1, 1]) return end @@ -53,22 +53,25 @@ 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.rowtable(start_value, x, :i1, :i2, :solution) - T = NamedTuple{(:i1, :i2, :solution),Tuple{Int,Int,Float64}} + start_table = Containers.rowtable(start_value, x; header = [:i1, :i2, :i3]) + T = NamedTuple{(:i1, :i2, :i3),Tuple{Int,Int,Float64}} @test start_table isa Vector{T} @test length(start_table) == length(x) - @test (i1 = 1, i2 = 1, solution = 0.0) in start_table - x_table = Containers.rowtable(x, :i1, :i2, :variable) - @test (i1 = 1, i2 = 1, variable = x[1, 1]) in x_table + @test (i1 = 1, i2 = 1, i3 = 0.0) in start_table + x_table = Containers.rowtable(x; header = [:i1, :i2, :i3]) + @test (i1 = 1, i2 = 1, i3 = 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.rowtable(x, :y, :a) - @test_throws ErrorException Containers.rowtable(x, :y, :a, :b, :c) - @test Containers.rowtable(x, :y, :a, :b) isa Vector{<:NamedTuple} + @test_throws ErrorException Containers.rowtable(x; header = [:y, :a]) + @test_throws( + ErrorException, + Containers.rowtable(x; header = [:y, :a, :b, :c]), + ) + @test Containers.rowtable(x; header = [:y, :a, :b]) isa Vector{<:NamedTuple} return end