From f58faaf3e07d0554a461c827c6bc028193c2b96e Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 10 Sep 2017 09:44:45 -0400 Subject: [PATCH] Container revamp (#1099) Replace JuMPDict with Dict. Rewrite JuMPArray to be compatible with AbstractArray. Explicit keyword argument in macro to force container type. --- src/JuMP.jl | 27 +---- src/JuMPArray.jl | 260 +++++++++++++++++++++++++++++++------------ src/JuMPContainer.jl | 244 ---------------------------------------- src/containers.jl | 105 +++++++++++++++++ src/macros.jl | 128 +++++++++++---------- src/operators.jl | 17 +-- src/print.jl | 259 ------------------------------------------ test/containers.jl | 162 +++++++++++++++++++++++++++ test/runtests.jl | 1 + test/variable.jl | 125 ++++++++++++--------- 10 files changed, 604 insertions(+), 724 deletions(-) delete mode 100644 src/JuMPContainer.jl create mode 100644 src/containers.jl create mode 100644 test/containers.jl diff --git a/src/JuMP.jl b/src/JuMP.jl index 09c8552311d..1873e4382db 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -63,7 +63,7 @@ export @objective, @NLobjective, @NLparameter, @constraintref -include("JuMPContainer.jl") + include("utils.jl") const MOIVAR = MOI.VariableReference @@ -164,7 +164,6 @@ mutable struct Model <: AbstractModel objdict::Dict{Symbol,Any} # dictionary from variable and constraint names to objects - map_counter::Int # number of times we call getvalue, getdual, getlowerbound and getupperbound on a JuMPContainer, so that we can print out a warning operator_counter::Int # number of times we add large expressions # Extension dictionary - e.g. for robust @@ -201,7 +200,6 @@ mutable struct Model <: AbstractModel m.nlpdata = nothing m.simplify_nonlinear_expressions = simplify_nonlinear_expressions m.objdict = Dict{Symbol,Any}() - m.map_counter = 0 m.operator_counter = 0 m.ext = Dict{Symbol,Any}() @@ -535,14 +533,6 @@ Base.copy(v::Variable, new_model::Model) = Variable(new_model, v.col) Base.copy(x::Void, new_model::Model) = nothing Base.copy(v::AbstractArray{Variable}, new_model::Model) = (var -> Variable(new_model, var.col)).(v) -# Copy methods for variable containers -Base.copy(d::JuMPContainer) = map(copy, d) -function Base.copy(d::JuMPContainer, new_model::Model) - new_d = map(x -> copy(x, new_model), d) - new_d.meta[:model] = new_model - new_d -end - ########################################################################## # ConstraintRef # Reference to a constraint for retrieving solution info @@ -760,20 +750,6 @@ function Base.setindex!(m::JuMP.Model, value, name::Symbol) end # usage warnings -function mapcontainer_warn(f, x::JuMPContainer, var_or_expr) - isempty(x) && return - v = first(values(x)) - m = v.m - m.map_counter += 1 - if m.map_counter > 400 - # It might not be f that was called the 400 first times but most probably it is f - Base.warn_once("$f has been called on a collection of $(var_or_expr)s a large number of times. For performance reasons, this should be avoided. Instead of $f(x)[a,b,c], use $f(x[a,b,c]) to avoid temporary allocations.") - end -end -mapcontainer_warn(f, x::JuMPContainer{Variable}) = mapcontainer_warn(f, x, "variable") -mapcontainer_warn{E}(f, x::JuMPContainer{E}) = mapcontainer_warn(f, x, "expression") -getvalue_warn(x::JuMPContainer) = nothing - function operator_warn(lhs::AffExpr,rhs::AffExpr) if length(lhs.vars) > 50 || length(rhs.vars) > 50 if length(lhs.vars) > 1 @@ -821,6 +797,7 @@ Base.ndims(::JuMPTypes) = 0 ########################################################################## +include("containers.jl") include("operators.jl") # include("writers.jl") include("macros.jl") diff --git a/src/JuMPArray.jl b/src/JuMPArray.jl index c83d87fa9e7..d6e3034047c 100644 --- a/src/JuMPArray.jl +++ b/src/JuMPArray.jl @@ -3,96 +3,214 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -immutable JuMPArray{T,N,NT} <: JuMPContainer{T,N} - innerArray::Array{T,N} - indexsets::NT - lookup::NTuple{N,Any} - meta::Dict{Symbol,Any} +# JuMPArray is inspired by the AxisArrays package. +# JuMPArray can be replaced with AxisArray once integer indices are no longer +# a special case. See discussions at: +# https://github.com/JuliaArrays/AxisArrays.jl/issues/117 +# https://github.com/JuliaArrays/AxisArrays.jl/issues/84 + + +struct JuMPArray{T,N,Ax} <: AbstractArray{T,N} + data::Array{T,N} + axes::Ax + lookup::Vector{Dict} # TODO: correctly type return type of the Dict as Int end -@generated function JuMPArray{T,N}(innerArray::Array{T,N}, indexsets::NTuple{N,Any}) - dicttuple = Expr(:tuple) +export JuMPArray + +function JuMPArray(data::Array{T,N}, axs...) where {T,N} + lookup = Vector{Dict}(N) for i in 1:N - inner = quote - idxset = indexsets[$i] - ret = Dict{eltype(idxset), Int}() - end - tupelem = indexsets.parameters[i] - if !(tupelem == UnitRange{Int} || tupelem == StepRange{Int}) - inner = quote - $inner - cnt = 1 - for x in idxset - ret[x] = cnt - cnt += 1 - end - ret + d = Dict{eltype(axs[i]),Int}() + cnt = 1 + for el in axs[i] + if haskey(d, el) + error("Repeated index $el. Index sets must have unique elements.") end + d[el] = cnt + cnt += 1 end - push!(dicttuple.args, inner) + lookup[i] = d end - :(JuMPArray(innerArray, indexsets, $dicttuple, Dict{Symbol,Any}())) + return JuMPArray(data, axs, lookup) end -Base.getindex(d::JuMPArray, ::Colon) = d.innerArray[:] +# TODO: use generated function to make this fast +function to_index(A::JuMPArray{T,N}, idx...) where {T,N} + return tuple((isa(i,Colon) ? Colon() : (k <= N ? A.lookup[k][i] : (((i == 1) ? 1 : error("invalid index $i")))) for (k,i) in enumerate(idx))...) +end -@generated function Base.getindex{T,N,NT}(d::JuMPArray{T,N,NT}, idx...) - if N != length(idx) - error("Indexed into a JuMPArray with $(length(idx)) indices (expected $N indices)") +# TODO: use generated function to make this fast and type stable +# TODO: better error (or just handle correctly) when user tries to index with a range like a:b +# The only kind of slicing we support is dropping a dimension with colons +function Base.getindex(A::JuMPArray{T}, idx...) where {T} + if Colon() in idx + JuMPArray(A.data[to_index(A,idx...)...], (ax for (i,ax) in enumerate(A.axes) if idx[i] == Colon())...) + else + return A.data[to_index(A,idx...)...]::T end - Expr(:call, :getindex, :(d.innerArray), _to_cartesian(d,NT,idx)...) end +Base.getindex(A::JuMPArray, idx::CartesianIndex) = A.data[idx] + +Base.setindex!(A::JuMPArray, v, idx...) = A.data[to_index(A,idx...)...] = v +Base.setindex!(A::JuMPArray, v, idx::CartesianIndex) = A.data[idx] = v + +# AbstractArray interface + +Base.linearindices(A::JuMPArray) = error("linearindices not defined for JuMPArray3") +Base.size(A::JuMPArray) = size(A.data) +Base.indices(A::JuMPArray) = A.axes -@generated function Base.setindex!{T,N,NT}(d::JuMPArray{T,N,NT}, v, idx...) - if N != length(idx) - error("Indexed into a JuMPArray with $(length(idx)) indices (expected $N indices)") +# Arbitrary typed indices. Linear indexing not supported. +struct IndexAnyCartesian <: Base.IndexStyle end +Base.IndexStyle(::Type{JuMPArray{T,N,Ax}}) where {T,N,Ax} = IndexAnyCartesian() + +Base.broadcast(f::Function, A::JuMPArray) = JuMPArray(broadcast(f, A.data), A.axes, A.lookup) + +Base.isempty(A::JuMPArray) = isempty(A.data) + +function Base.isassigned(A::JuMPArray, idx...) + try + to_index(idx...) + return true + catch + return false + end +end +# For ambiguity +function Base.isassigned(A::JuMPArray, idx::Int...) + try + to_index(idx...) + return true + catch + return false end - Expr(:call, :setindex!, :(d.innerArray), :v, _to_cartesian(d,NT,idx)...) end -function _to_cartesian(d,NT,idx...) - indexing = Any[] - for (i,S) in enumerate(NT.parameters) - idxtype = idx[1][i] - if S == UnitRange{Int} - if idxtype == Colon - # special stuff - push!(indexing, Colon()) - elseif idxtype <: Range - push!(indexing, quote - rng = d.indexsets[$i] - I = idx[$i] - I - (start(rng) - 1) - end) - else - push!(indexing, quote - rng = d.indexsets[$i] - I = idx[$i] - first(rng) <= I <= last(rng) || error("Failed attempt to index JuMPArray along dimension $($i): $I ∉ $(d.indexsets[$i])") - I - (start(rng) - 1) - end) - end - elseif S == StepRange{Int} - if idx[1][i] == Colon - push!(indexing, Colon()) +Base.eachindex(A::JuMPArray) = CartesianRange(size(A.data)) + +# TODO: similar + +# Adapted printing from Julia's show.jl + +# Copyright (c) 2009-2016: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, +# and other contributors: +# +# https://github.com/JuliaLang/julia/contributors +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +function summaryio(io::IO, A::JuMPArray) + _summary(io, A) + for (k,ax) in enumerate(A.axes) + print(io, " Dimension $k, ") + show(IOContext(io, :limit=>true), ax) + println(io) + end + print(io, "And data, a ", summary(A.data)) +end +_summary(io, A::JuMPArray{T,N}) where {T,N} = println(io, "$N-dimensional JuMPArray{$T,$N,...} with index sets:") + +function Base.summary(A::JuMPArray) + io = IOBuffer() + summaryio(io, A) + String(io) +end + +function Base.showarray(io::IO, X::JuMPArray, repr::Bool = true; header = true) + repr = false + #if repr && ndims(X) == 1 + # return Base.show_vector(io, X, "[", "]") + #end + if !haskey(io, :compact) + io = IOContext(io, :compact => true) + end + if !repr && get(io, :limit, false) && eltype(X) === Method + # override usual show method for Vector{Method}: don't abbreviate long lists + io = IOContext(io, :limit => false) + end + (!repr && header) && print(io, summary(X)) + if !isempty(X.data) + (!repr && header) && println(io, ":") + if ndims(X.data) == 0 + if isassigned(X.data) + return show(io, X.data[]) else - push!(indexing, quote - rng = $(d.indexsets[i]) - I = idx[$i] - first(rng) <= I <= last(rng) || error("Failed attempt to index JuMPArray along dimension $($i): $I ∉ $(d.indexsets[$i])") - dv, rv = divrem(I - start(rng), step(rng)) - rv == 0 || error("Failed attempt to index JuMPArray along dimension $($i): $I ∉ $(d.indexsets[$i])") - dv + 1 - end) + return print(io, undef_ref_str) end + end + #if repr + # if ndims(X.data) <= 2 + # Base.print_matrix_repr(io, X) + # else + # show_nd(io, X, print_matrix_repr, false) + # end + #else + punct = (" ", " ", "") + if ndims(X.data) <= 2 + Base.print_matrix(io, X.data, punct...) else - push!(indexing, quote - if !haskey(d.lookup[$i],idx[$i]) - error("Failed attempt to index JuMPArray along dimension $($i): $(idx[$i]) ∉ $(d.indexsets[$i])") + show_nd(io, X, + (io, slice) -> Base.print_matrix(io, slice, punct...), + !repr) + end + #end + elseif repr + Base.repremptyarray(io, X.data) + end +end + +# n-dimensional arrays +function show_nd(io::IO, a::JuMPArray, print_matrix, label_slices) + limit::Bool = get(io, :limit, false) + if isempty(a) + return + end + tailinds = Base.tail(Base.tail(indices(a.data))) + nd = ndims(a)-2 + for I in CartesianRange(tailinds) + idxs = I.I + if limit + for i = 1:nd + ii = idxs[i] + ind = tailinds[i] + if length(ind) > 10 + if ii == ind[4] && all(d->idxs[d]==first(tailinds[d]),1:i-1) + for j=i+1:nd + szj = size(a,j+2) + indj = tailinds[j] + if szj>10 && first(indj)+2 < idxs[j] <= last(indj)-3 + @goto skip + end + end + #println(io, idxs) + print(io, "...\n\n") + @goto skip + end + if ind[3] < ii <= ind[end-3] + @goto skip + end end - d.lookup[$i][idx[$i]]::Int - end) + end + end + if label_slices + print(io, "[:, :, ") + for i = 1:(nd-1); show(io, a.axes[i+2][idxs[i]]); print(io,", "); end + show(io, a.axes[end][idxs[end]]) + println(io, "] =") end + slice = view(a.data, indices(a.data,1), indices(a.data,2), idxs...) + Base.print_matrix(io, slice) + print(io, idxs == map(last,tailinds) ? "" : "\n\n") + @label skip end - indexing end diff --git a/src/JuMPContainer.jl b/src/JuMPContainer.jl deleted file mode 100644 index 8cc78684f79..00000000000 --- a/src/JuMPContainer.jl +++ /dev/null @@ -1,244 +0,0 @@ -# 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 http://mozilla.org/MPL/2.0/. - -using Base.Meta -# multivarate "dictionary" used for collections of variables/constraints - -abstract type JuMPContainer{T,N} end - -include("JuMPArray.jl") - -type IndexPair - idxvar - idxset -end - -type JuMPDict{T,N} <: JuMPContainer{T,N} - tupledict::Dict{NTuple{N,Any},T} - meta::Dict{Symbol,Any} - - (::Type{JuMPDict{T,N}}){T,N}() = new{T,N}(Dict{NTuple{N,Any},T}(), Dict{Symbol,Any}()) -end - -function JuMPDict{T,N}(d::Dict{NTuple{N,Any},T}) - tmp = JuMPDict{T,N}() - tmp.tupledict = d - tmp -end - -function JuMPDict{T,N}(d::Dict{NTuple{N,Any},T}, meta::Dict{Symbol,Any}) - tmp = JuMPDict{T,N}() - tmp.tupledict = d - tmp.meta = meta - tmp -end - -# type JuMPContainerData -# name -# indexsets -# indexexprs::Vector{IndexPair} -# condition -# end -# -# # Needed by getvaluewarn when called by _mapInner -# getname(data::JuMPContainerData) = data.name - -#JuMPDict{T,N}(name::AbstractString) = -# JuMPDict{T,N}(Dict{NTuple{N},T}(), name) - -Base.getindex(d::JuMPDict, t...) = d.tupledict[t] -Base.setindex!(d::JuMPDict, value, t...) = (d.tupledict[t] = value) - -Base.map(f::Function, d::JuMPArray) = - JuMPArray(map(f, d.innerArray), d.indexsets, d.lookup, copy(d.meta)) - -function Base.map{T,N}(f::Function, d::JuMPDict{T,N}) - ret = Base.return_types(f, Tuple{T}) - R = (length(ret) == 1 ? ret[1] : Any) - x = JuMPDict{R,N}() - for (k,v) in d.tupledict - x.tupledict[k] = f(v) - end - x.meta = copy(d.meta) - return x -end - -Base.isempty(d::JuMPContainer) = isempty(_innercontainer(d)) - -coloncheck(args::Number...) = nothing - -function coloncheck(args...) - if any(t -> t == Colon(), args) - error("Colons not allowed as keys in JuMP containers.") - end -end - -# generate and instantiate a type which is indexed by the given index sets -# the following types of index sets are allowed: -# 0:K -- range with compile-time starting index -# S -- general iterable set -function gendict(instancename,T,idxsets...) - N = length(idxsets) - truearray = true - for idxset in idxsets - s = isexpr(idxset,:escape) ? idxset.args[1] : idxset - if !(isexpr(s,:(:)) && length(s.args) == 2 && s.args[1] == 1) - truearray = false - break - end - end - sizes = Expr(:tuple, [:(length($rng)) for rng in idxsets]...) - if truearray - :($instancename = Array{$T}($sizes...)) - else - indexsets = Expr(:tuple, idxsets...) - :($instancename = JuMPArray(Array{$T}($sizes...), $indexsets)) - end -end - -metadata(x::Union{JuMPArray,JuMPDict}) = x.meta -metadata{T<:JuMPContainer}(::T) = error("Type $T has no field meta. This field is used to store metadata such as the JuMP.Model at the key :model.") -pushmeta!(x::JuMPContainer, sym::Symbol, val) = (metadata(x)[sym] = val) -getmeta(x::JuMPContainer, sym::Symbol) = metadata(x)[sym] -hasmeta(x::JuMPContainer, sym::Symbol) = haskey(metadata(x), sym) - -# TODO do we want this or should we use broadcast syntax? -# for accessor in (:getdual, :getlowerbound, :getupperbound, :getstart) -# @eval $accessor(x::AbstractArray) = map($accessor,x) -# @eval $accessor(x::JuMPContainer) = map($accessor,x) -# end - - -_similar(x::Array) = Array{Float64}(size(x)) -_similar{T}(x::Dict{T}) = Dict{T,Float64}() - -_innercontainer(x::JuMPArray) = x.innerArray -_innercontainer(x::JuMPDict) = x.tupledict - -JuMPContainer_from(x::JuMPDict,inner) = JuMPDict(inner) -JuMPContainer_from(x::JuMPArray,inner) = JuMPArray(inner, x.indexsets) - -# delegate zero-argument functions -for f in (:(Base.ndims), :(Base.length), :(Base.abs)) - @eval $f(x::JuMPArray) = $f(x.innerArray) -end - -Base.length(x::JuMPDict) = length(x.tupledict) - -Base.ndims{T,N}(x::JuMPDict{T,N}) = N -Base.abs(x::JuMPDict) = map(abs, x) -# avoid dangerous behavior with "end" (#730) -Base.endof(x::JuMPArray) = error("endof() (and \"end\" syntax) not implemented for JuMPArray objects.") -Base.size(x::JuMPArray) = error(string("size (and \"end\" syntax) not implemented for JuMPArray objects.", -"Use JuMP.size if you want to access the dimensions.")) -Base.size(x::JuMPArray,k) = error(string("size (and \"end\" syntax) not implemented for JuMPArray objects.", -" Use JuMP.size if you want to access the dimensions.")) -size(x::JuMPArray) = size(x.innerArray) -size(x::JuMPArray,k) = size(x.innerArray,k) -# for uses of size() within JuMP -size(x) = Base.size(x) -size(x,k) = Base.size(x,k) -# delegate one-argument functions -Base.issymmetric(x::JuMPArray) = issymmetric(x.innerArray) - -Base.eltype{T}(x::JuMPContainer{T}) = T - -Base.full(x::JuMPContainer) = x - -# keys/vals iterations for JuMPContainers -Base.keys(d::JuMPDict) = keys(d.tupledict) -Base.values(d::JuMPDict) = values(d.tupledict) - -Base.keys(d::JuMPArray) = KeyIterator(d) -Base.values(d::JuMPArray) = ValueIterator(d.innerArray) - -# Wrapper type so that you can't access the values directly -type ValueIterator{T,N} - x::Array{T,N} -end -Base.start(it::ValueIterator) = start(it.x) -Base.next(it::ValueIterator, k) = next(it.x, k) -Base.done(it::ValueIterator, k) = done(it.x, k) -Base.length(it::ValueIterator) = length(it.x) - -type KeyIterator{JA<:JuMPArray} - x::JA - dim::Int - next_k_cache::Array{Any,1} - function (::Type{KeyIterator{JA}}){JA}(d) - n = ndims(d.innerArray) - new{JA}(d, n, Array{Any}(n+1)) - end -end - -KeyIterator{JA}(d::JA) = KeyIterator{JA}(d) - -function indexability(x::JuMPArray) - for i in 1:length(x.indexsets) - if !method_exists(getindex, (typeof(x.indexsets[i]),)) - return false - end - end - - return true -end - -function Base.start(it::KeyIterator) - if indexability(it.x) - return start(it.x.innerArray) - else - return notindexable_start(it.x) - end -end - -@generated function notindexable_start{T,N,NT}(x::JuMPArray{T,N,NT}) - quote - $(Expr(:tuple, 0, [:(start(x.indexsets[$i])) for i in 1:N]...)) - end -end - -@generated function _next{T,N,NT}(x::JuMPArray{T,N,NT}, k::Tuple) - quote - $(Expr(:tuple, [:(next(x.indexsets[$i], k[$i+1])[1]) for i in 1:N]...)) - end -end - -function Base.next(it::KeyIterator, k::Tuple) - cartesian_key = _next(it.x, k) - pos = -1 - for i in 1:it.dim - if !done(it.x.indexsets[i], next(it.x.indexsets[i], k[i+1])[2] ) - pos = i - break - end - end - if pos == - 1 - it.next_k_cache[1] = 1 - return cartesian_key, tuple(it.next_k_cache...) - end - it.next_k_cache[1] = 0 - for i in 1:it.dim - if i < pos - it.next_k_cache[i+1] = start(it.x.indexsets[i]) - elseif i == pos - it.next_k_cache[i+1] = next(it.x.indexsets[i], k[i+1])[2] - else - it.next_k_cache[i+1] = k[i+1] - end - end - cartesian_key, tuple(it.next_k_cache...) -end - -Base.done(it::KeyIterator, k::Tuple) = (k[1] == 1) - -@generated __next{T,N,NT}(x::JuMPArray{T,N,NT}, k::Integer) = - quote - subidx = ind2sub(size(x),k) - $(Expr(:tuple, [:(x.indexsets[$i][subidx[$i]]) for i in 1:N]...)), next(x.innerArray,k)[2] - end -Base.next(it::KeyIterator, k) = __next(it.x,k::Integer) -Base.done(it::KeyIterator, k) = done(it.x.innerArray, k::Integer) - -Base.length(it::KeyIterator) = length(it.x.innerArray) diff --git a/src/containers.jl b/src/containers.jl new file mode 100644 index 00000000000..4ea48bc016a --- /dev/null +++ b/src/containers.jl @@ -0,0 +1,105 @@ +# 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 http://mozilla.org/MPL/2.0/. + +include("JuMPArray.jl") + +validarrayindexset(::Base.OneTo) = true +validarrayindexset(r::UnitRange) = start(r) == 1 +validarrayindexset(s) = false + +""" + generatecontainer(T, indexvars, indexsets, requestedtype) + +Return a tuple, the first element of which is *code* that generates a container +for objects of type `T` given the index variables, +index sets, and `requestedtype`. `requestedtype` may be +one of `:Array`, `:JuMPArray`, `:Dict`, or `:Auto`. Return error-producing code if +requested type is incompatible. For the case of `:Auto`, the following rules are +used to determine the appropriate container: + +1. If all index sets are either explicit `Expr(:(:), 1, B)` objects for any `B` or symbols which refer to objects of type `Base.OneTo`, then an `Array` is generated of the appropriate size. Types of symbols/expressions are not known at compile time, so we defer to type-safe functions to check the `Base.OneTo` condition. + +2. If condition (1) does not hold, and the index sets are independent (the index variable for one set does not appear in the definition of another), then an `JuMPArray` is generated of the appropriate size. + +3. Otherwise, generate an empty `Dict{Any,T}`. + +The second element of the return tuple is a `Bool`, `true` if the container type +automatically checks for duplicate terms in the index sets and `false` otherwise. + +### Examples + + generatecontainer(Variable, [:i,:j], [:(1:N), :(1:T)], :Auto) + # Returns code equivalent to: + # :(Array{Variable}(length(1:N), length(1:T)) + + generatecontainer(Variable, [:i,:j], [:(1:N), :(2:T)], :Auto) + # Returns code equivalent to: + # :(JuMPArray(Array{Variable}(length(1:N), length(2:T)), \$indexvars...)) + + generatecontainer(Variable, [:i,:j], [:(1:N), :(S)], :Auto) + # Returns code that generates an Array if S is of type Base.OneTo, + # otherwise an JuMPArray. + + generatecontainer(Variable, [:i,:j], [:(1:N), :(1:j)], :Auto) + # Returns code equivalent to: + # :(Dict{Any,Variable}()) +""" +function generatecontainer(T, indexvars, indexsets, requestedtype) + hasdependent = hasdependentsets(indexvars,indexsets) + onetosets = falses(length(indexsets)) + for (i,indexset) in enumerate(indexsets) + s = isexpr(indexset,:escape) ? indexset.args[1] : indexset + if isexpr(s,:(:)) && length(s.args) == 2 && s.args[1] == 1 + onetosets[i] = true + end + end + + if requestedtype == :Dict || (requestedtype == :Auto && hasdependent) + return :(Dict{Any,$T}()), false + end + + sizes = Expr(:tuple, [:(length($rng)) for rng in indexsets]...) + arrayexpr = :(Array{$T}($sizes...)) + + if requestedtype == :Array + hasdependent && return :(error("Unable to create requested Array because index sets are dependent.")), true + if all(onetosets) + return arrayexpr, true + else + # all sets must be one-based intervals + condition = Expr(:&&) + for (i,indexset) in enumerate(indexsets) + if !onetosets[i] + push!(condition.args, Expr(:call, :(JuMP.validarrayindexset), indexset)) + end + end + return :($condition || error("Index set for array is not one-based interval."); $arrayexpr), true + end + end + + axisexpr = :(JuMP.JuMPArray(Array{$T}($sizes...))) + append!(axisexpr.args, indexsets) + + if requestedtype == :JuMPArray + hasdependent && return :(error("Unable to create requested JuMPArray because index sets are dependent.")), true + return axisexpr, true + end + + @assert requestedtype == :Auto + if all(onetosets) # definitely Array + return arrayexpr, true + end + + # Fallback, we have to decide once we know the types of the symbolic sets + condition = Expr(:&&) + for (i,indexset) in enumerate(indexsets) + if !onetosets[i] + push!(condition.args, Expr(:call, :isa, indexset, :(Base.OneTo))) + end + end + + # TODO: check type stability + return :(if $condition; $arrayexpr; else $axisexpr; end), true +end diff --git a/src/macros.jl b/src/macros.jl index a5619424bb6..52a38bae849 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -19,11 +19,10 @@ include("parseexpr.jl") # Unexported. Takes as input an object representing a name, associated index # sets, and conditions on those sets, for example # buildrefsets(:(x[i=1:3,[:red,:blue]],k=S; i+k <= 6)) -# Used internally in macros to build JuMPContainers and constraints. Returns +# Used internally in macros to build containers and constraints. Returns # refcall: Expr to reference a particular element, e.g. :(x[i,j,k]) -# idxvars: Index names used in referencing, e.g.g {:i,:j,:k} -# idxsets: Index sets for indexing, e.g. {1:3, [:red,:blue], S} -# idxpairs: Vector of IndexPair +# idxvars: Index names used in referencing, e.g.g []:i,:j,:k] +# idxsets: Index sets for indexing, e.g. [1:3, [:red,:blue], S] # condition: Expr containing any condition present for indexing # Note in particular that it does not actually evaluate the condition, and so # it returns just the cartesian product of possible indices. @@ -31,7 +30,6 @@ function buildrefsets(expr::Expr, cname) c = copy(expr) idxvars = Any[] idxsets = Any[] - idxpairs = IndexPair[] # Creating an indexed set of refs refcall = Expr(:ref, cname) if isexpr(c, :typed_vcat) || isexpr(c, :ref) @@ -53,22 +51,20 @@ function buildrefsets(expr::Expr, cname) parse_done, idxvar, _idxset = tryParseIdxSet(s::Expr) if parse_done idxset = esc(_idxset) - push!(idxpairs, IndexPair(idxvar, _idxset)) end end if !parse_done # No index variable specified idxvar = gensym() idxset = esc(s) - push!(idxpairs, IndexPair(nothing,s)) end push!(idxvars, idxvar) push!(idxsets, idxset) push!(refcall.args, esc(idxvar)) end - return refcall, idxvars, idxsets, idxpairs, condition + return refcall, idxvars, idxsets, condition end -buildrefsets(c, cname) = (cname, Any[], Any[], IndexPair[], :()) +buildrefsets(c, cname) = (cname, Any[], Any[], :()) buildrefsets(c) = buildrefsets(c, getname(c)) ############################################################################### @@ -83,10 +79,11 @@ buildrefsets(c) = buildrefsets(c, getname(c)) # If none, pass :(). # idxvars: As defined for buildrefsets # idxsets: As defined for buildrefsets -# idxpairs: As defined for buildrefsets # sym: A symbol or expression containing the element type of the # resulting container, e.g. :AffExpr or :Variable -function getloopedcode(varname, code, condition, idxvars, idxsets, idxpairs, sym; lowertri=false) +# requestedcontainer: `:Auto`, `:Array`, `:JuMPArray`, `:Dict` passed +# through to generatecontainer() +function getloopedcode(varname, code, condition, idxvars, idxsets, sym, requestedcontainer::Symbol; lowertri=false) # if we don't have indexing, just return to avoid allocating stuff if isempty(idxsets) @@ -95,10 +92,18 @@ function getloopedcode(varname, code, condition, idxvars, idxsets, idxpairs, sym hascond = (condition != :()) + if hascond + if requestedcontainer == :Auto + requestedcontainer = :Dict + elseif requestedcontainer == :Array || requestedcontainer == :JuMPArray + return :(error("Requested container type is incompatible with conditional indexing. Use :Dict or :Auto instead.")) + end + end + containercode, autoduplicatecheck = generatecontainer(sym, idxvars, idxsets, requestedcontainer) + if lowertri @assert !hascond @assert length(idxvars) == 2 - @assert length(idxpairs) == 2 @assert !hasdependentsets(idxvars, idxsets) i, j = esc(idxvars[1]), esc(idxvars[2]) @@ -119,6 +124,19 @@ function getloopedcode(varname, code, condition, idxvars, idxsets, idxpairs, sym end end else + if !autoduplicatecheck # we need to check for duplicate keys in the index set + if length(idxvars) > 1 + keytuple = Expr(:tuple, esc.(idxvars)...) + else + keytuple = esc(idxvars[1]) + end + code = quote + if haskey($varname, $keytuple) + error(string("Repeated index ", $keytuple,". Index sets must have unique elements.")) + end + $code + end + end if hascond code = quote $(esc(condition)) || continue @@ -136,15 +154,10 @@ function getloopedcode(varname, code, condition, idxvars, idxsets, idxpairs, sym end end end - if hascond || hasdependentsets(idxvars,idxsets) - # force a JuMPDict - N = length(idxsets) - mac = :($varname = JuMPDict{$(sym),$N}()) - else - mac = gendict(varname, sym, idxsets...) - end + + return quote - $mac + $varname = $containercode $code nothing end @@ -344,7 +357,7 @@ macro constraint(args...) # Strategy: build up the code for non-macro addconstraint, and if needed # we will wrap in loops to assign to the ConstraintRefs - refcall, idxvars, idxsets, idxpairs, condition = buildrefsets(c, variable) + refcall, idxvars, idxsets, condition = buildrefsets(c, variable) # JuMP accepts constraint syntax of the form @constraint(m, foo in bar). # This will be rewritten to a call to constructconstraint!(foo, bar). To # extend JuMP to accept set-based constraints of this form, it is necessary @@ -435,7 +448,7 @@ macro constraint(args...) " expr1 == expr2\n" * " lb <= expr <= ub")) end return assert_validmodel(m, quote - $(getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, :ConstraintRef)) + $(getloopedcode(variable, code, condition, idxvars, idxsets, :ConstraintRef, :Auto)) $(if anonvar variable else @@ -777,7 +790,7 @@ macro expression(args...) variable = gensym() escvarname = anonvar ? variable : esc(getname(c)) - refcall, idxvars, idxsets, idxpairs, condition = buildrefsets(c, variable) + refcall, idxvars, idxsets, condition = buildrefsets(c, variable) newaff, parsecode = parseExprToplevel(x, :q) code = quote q = 0.0 @@ -793,7 +806,7 @@ macro expression(args...) $code $(refcall) = $newaff end - code = getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, :AffExpr) + code = getloopedcode(variable, code, condition, idxvars, idxsets, :AffExpr, :Auto) if m === nothing # deprecated usage return quote $code @@ -997,7 +1010,7 @@ macro variable(args...) variable = gensym() quotvarname = anonvar ? :(:__anon__) : quot(getname(var)) escvarname = anonvar ? variable : esc(getname(var)) - basename = string(getname(var)) + basename = anonvar ? "__anon__" : string(getname(var)) if !isa(getname(var),Symbol) && !anonvar Base.error("Expression $(getname(var)) should not be used as a variable name. Use the \"anonymous\" syntax $(getname(var)) = @variable(m, ...) instead.") @@ -1008,6 +1021,7 @@ macro variable(args...) obj = nothing binary = false integer = false + container = :Auto extra_kwargs = [] for ex in kwargs kwarg = ex.args[1] @@ -1028,6 +1042,9 @@ macro variable(args...) integer = esc_nonconstant(ex.args[2]) elseif kwarg == :binary binary = esc_nonconstant(ex.args[2]) + elseif kwarg == :container + container = ex.args[2] + container in [:Auto, :Array, :JuMPArray, :Dict] || _error("Invalid container type $container. Must be Auto, Array, JuMPArray, or Dict.") else push!(extra_kwargs, ex) end @@ -1070,7 +1087,7 @@ macro variable(args...) # We now build the code to generate the variables (and possibly the JuMPDict # to contain them) - refcall, idxvars, idxsets, idxpairs, condition = buildrefsets(var, variable) + refcall, idxvars, idxsets, condition = buildrefsets(var, variable) clear_dependencies(i) = (isdependent(idxvars,idxsets[i],i) ? () : idxsets[i]) # Code to be used to create each variable of the container. @@ -1109,7 +1126,7 @@ macro variable(args...) end return assert_validmodel(m, quote $(esc(idxsets[1].args[1].args[2])) == $(esc(idxsets[2].args[1].args[2])) || error("Cannot construct symmetric variables with nonsquare dimensions") - $(getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, vartype; lowertri=symmetric)) + $(getloopedcode(variable, code, condition, idxvars, idxsets, vartype, container; lowertri=symmetric)) $(if sdp quote JuMP.addconstraint($m, JuMP._constructconstraint!($variable, JuMP.PSDCone())) @@ -1119,37 +1136,34 @@ macro variable(args...) $(anonvar ? variable : :($escvarname = $variable)) end) else - coloncheckcode = Expr(:call,:coloncheck,refcall.args[2:end]...) - code = :($coloncheckcode; $code) return assert_validmodel(m, quote - $(getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, vartype)) - isa($variable, JuMPContainer) && pushmeta!($variable, :model, $m) + $(getloopedcode(variable, code, condition, idxvars, idxsets, vartype, container)) !$anonvar && registervar($m, $quotvarname, $variable) $(anonvar ? variable : :($escvarname = $variable)) end) end end -macro constraintref(var) - if isa(var,Symbol) - # easy case - return esc(:(local $var)) - else - if !isexpr(var,:ref) - error("Syntax error: Expected $var to be of form var[...]") - end - - varname = var.args[1] - idxsets = var.args[2:end] - idxpairs = IndexPair[] - - code = quote - $(esc(gendict(varname, :ConstraintRef, idxsets...))) - nothing - end - return code - end -end +# TODO: replace with a general macro that can construct any container type +# macro constraintref(var) +# if isa(var,Symbol) +# # easy case +# return esc(:(local $var)) +# else +# if !isexpr(var,:ref) +# error("Syntax error: Expected $var to be of form var[...]") +# end +# +# varname = var.args[1] +# idxsets = var.args[2:end] +# +# code = quote +# $(esc(gendict(varname, :ConstraintRef, idxsets...))) +# nothing +# end +# return code +# end +# end macro NLobjective(m, sense, x) m = esc(m) @@ -1184,7 +1198,7 @@ macro NLconstraint(m, x, extra...) # Strategy: build up the code for non-macro addconstraint, and if needed # we will wrap in loops to assign to the ConstraintRefs - refcall, idxvars, idxsets, idxpairs, condition = buildrefsets(c, variable) + refcall, idxvars, idxsets, condition = buildrefsets(c, variable) # Build the constraint if isexpr(x, :call) # one-sided constraint # Simple comparison - move everything to the LHS @@ -1230,7 +1244,7 @@ macro NLconstraint(m, x, extra...) " expr1 <= expr2\n" * " expr1 >= expr2\n" * " expr1 == expr2") end - looped = getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, :(ConstraintRef{Model,NonlinearConstraint})) + looped = getloopedcode(variable, code, condition, idxvars, idxsets, :(ConstraintRef{Model,NonlinearConstraint}), :Auto) return assert_validmodel(m, quote initNLP($m) $m.internalModelLoaded = false @@ -1264,12 +1278,12 @@ macro NLexpression(args...) variable = gensym() escvarname = anonvar ? variable : esc(getname(c)) - refcall, idxvars, idxsets, idxpairs, condition = buildrefsets(c, variable) + refcall, idxvars, idxsets, condition = buildrefsets(c, variable) code = quote $(refcall) = NonlinearExpression($m, @processNLExpr($m, $(esc(x)))) end return assert_validmodel(m, quote - $(getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, :NonlinearExpression)) + $(getloopedcode(variable, code, condition, idxvars, idxsets, :NonlinearExpression, :Auto)) $(anonvar ? variable : :($escvarname = $variable)) end) end @@ -1290,12 +1304,12 @@ macro NLparameter(m, ex) variable = gensym() escvarname = anonvar ? variable : esc(getname(c)) - refcall, idxvars, idxsets, idxpairs, condition = buildrefsets(c, variable) + refcall, idxvars, idxsets, condition = buildrefsets(c, variable) code = quote $(refcall) = newparameter($m, $(esc(x))) end return assert_validmodel(m, quote - $(getloopedcode(variable, code, condition, idxvars, idxsets, idxpairs, :NonlinearParameter)) + $(getloopedcode(variable, code, condition, idxvars, idxsets, :NonlinearParameter, :Auto)) $(anonvar ? variable : :($escvarname = $variable)) end) end diff --git a/src/operators.jl b/src/operators.jl index 6501188786c..9b8e53f91b9 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -206,10 +206,7 @@ _sizehint_expr!(q, n) = nothing # - dot ############################################################################# -Base.sum(j::JuMPArray) = sum(j.innerArray) -Base.sum(j::JuMPDict) = sum(values(j.tupledict)) -Base.sum(j::JuMPArray{Variable}) = AffExpr(vec(j.innerArray), ones(length(j.innerArray)), 0.0) -Base.sum(j::JuMPDict{Variable}) = AffExpr(collect(values(j.tupledict)), ones(length(j.tupledict)), 0.0) +# TODO: specialize sum for Dict and AxisArray of JuMP objects? Base.sum(j::Array{Variable}) = AffExpr(vec(j), ones(length(j)), 0.0) Base.sum(j::AbstractArray{Variable}) = sum([j[i] for i in eachindex(j)]) # to handle non-one-indexed arrays. function Base.sum{T<:GenericAffExpr}(affs::AbstractArray{T}) @@ -222,8 +219,6 @@ end import Base.vecdot -_dot_depr() = warn("dot is deprecated for multidimensional arrays. Use vecdot instead.") - # Base Julia's generic fallback vecdot requires that dot be defined # for scalars, so instead of defining them one-by-one, we will # fallback to the multiplication operator @@ -231,13 +226,6 @@ Base.dot(lhs::JuMPTypes, rhs::JuMPTypes) = lhs*rhs Base.dot(lhs::JuMPTypes, rhs::Number) = lhs*rhs Base.dot(lhs::Number, rhs::JuMPTypes) = lhs*rhs -Base.dot{T,S,N}(lhs::AbstractArray{T,N}, rhs::JuMPArray{S,N}) = begin _dot_depr(); vecdot(lhs,rhs); end -Base.dot{T,S,N}(lhs::JuMPArray{T,N},rhs::AbstractArray{S,N}) = begin _dot_depr(); vecdot(lhs,rhs); end -Base.dot{T,S,N}(lhs::JuMPArray{T,N},rhs::JuMPArray{S,N}) = begin _dot_depr(); vecdot(lhs,rhs); end -Base.dot{T<:JuMPTypes,S,N}(lhs::AbstractArray{T,N}, rhs::AbstractArray{S,N}) = begin _dot_depr(); vecdot(lhs,rhs); end -Base.dot{T<:JuMPTypes,S<:JuMPTypes,N}(lhs::AbstractArray{T,N}, rhs::AbstractArray{S,N}) = begin _dot_depr(); vecdot(lhs,rhs); end -Base.dot{T,S<:JuMPTypes,N}(lhs::AbstractArray{T,N}, rhs::AbstractArray{S,N}) = begin _dot_depr(); vecdot(lhs,rhs); end - Base.dot{T<:JuMPTypes,S<:JuMPTypes}(lhs::AbstractVector{T},rhs::AbstractVector{S}) = _dot(lhs,rhs) Base.dot{T<:JuMPTypes,S}(lhs::AbstractVector{T},rhs::AbstractVector{S}) = _dot(lhs,rhs) Base.dot{T,S<:JuMPTypes}(lhs::AbstractVector{T},rhs::AbstractVector{S}) = _dot(lhs,rhs) @@ -267,10 +255,7 @@ Base.promote_rule{R<:Real}(::Type{AffExpr}, ::Type{R} ) = AffExpr Base.promote_rule( ::Type{AffExpr}, ::Type{QuadExpr}) = QuadExpr Base.promote_rule{R<:Real}(::Type{QuadExpr},::Type{R} ) = QuadExpr -_throw_transpose_error() = error("Transpose not currently implemented for JuMPArrays with arbitrary index sets.") Base.transpose(x::AbstractJuMPScalar) = x -Base.transpose( x::JuMPArray) = _throw_transpose_error() -Base.ctranspose(x::JuMPArray) = _throw_transpose_error() # Can remove the following code once == overloading is removed diff --git a/src/print.jl b/src/print.jl index a323d2dc1e0..bd0797df0fe 100644 --- a/src/print.jl +++ b/src/print.jl @@ -318,265 +318,6 @@ function var_str(::Type{IJuliaMode}, v::Variable; mathmode=true) end -# #------------------------------------------------------------------------ -# ## JuMPContainer{Variable} -# #------------------------------------------------------------------------ -# Base.show(io::IO, j::Union{JuMPContainer{Variable}, Array{Variable}}) = print(io, cont_str(REPLMode,j)) -# Base.show(io::IO, ::MIME"text/latex", j::Union{JuMPContainer{Variable},Array{Variable}}) = -# print(io, cont_str(IJuliaMode,j,mathmode=false)) -# # Generic string converter, called by mode-specific handlers -# -# # Assumes that !isempty(j) -# _getmodel(j::Array{Variable}) = first(j).m -# _getmodel(j::JuMPContainer) = getmeta(j, :model) -# -# function cont_str(mode, j, sym::PrintSymbols) -# # Check if anything in the container -# if isempty(j) -# name = isa(j, JuMPContainer) ? printdata(j).name : "Empty Array{Variable}" -# return "$name (no indices)" -# end -# -# m = _getmodel(j) -# -# # If this looks like a user-created Array, then defer to base printing -# if !haskey(m.vardata, j) -# @assert isa(j, Array{Variable}) -# if ndims(j) == 1 -# return sprint((io,v) -> Base.show_vector(io, v, "[", "]"), j) -# else -# return sprint((io,X) -> Base.showarray(io, X), j) -# end -# end -# -# data = printdata(j) -# -# # 1. construct the part with variable name and indexing -# locvars = map(data.indexexprs) do tmp -# var = tmp.idxvar -# if var == nothing -# return "" -# else -# return string(var) -# end -# end -# num_dims = length(data.indexsets) -# idxvars = Array{String}(num_dims) -# dimidx = 1 -# for i in 1:num_dims -# if data.indexexprs[i].idxvar == nothing -# while DIMS[dimidx] in locvars -# dimidx += 1 -# end -# if dimidx > length(DIMS) -# error("Unexpectedly ran out of indices") -# end -# idxvars[i] = DIMS[dimidx] -# dimidx += 1 -# else -# idxvars[i] = locvars[i] -# end -# end -# name_idx = string(data.name, sym[:ind_open], join(idxvars,","), sym[:ind_close]) -# # 2. construct part with what we index over -# idx_sets = sym[:for_all]*" "*join(map(dim->string(idxvars[dim], " ", sym[:in], -# " ", sym[:open_set], -# cont_str_set(data.indexsets[dim],sym[:dots]), -# sym[:close_set]), 1:num_dims), ", ") -# # 3. Handle any conditions -# if isa(j, JuMPDict) && data.condition != :() -# idx_sets *= string(" s.t. ",join(parse_conditions(data.condition), " and ")) -# end -# -# # 4. Bounds and category, if possible, and return final string -# a_var = first(_values(j)) -# model = a_var.m -# var_cat = model.colCat[a_var.col] -# var_lb = model.colLower[a_var.col] -# var_ub = model.colUpper[a_var.col] -# # Variables may have different bounds, so we can't really print nicely -# # at this time (possibly ever, as they could have been changed post -# # creation, which we'd never be able to handle. -# all_same_lb = true -# all_same_ub = true -# for var in _values(j) -# all_same_lb &= model.colLower[var.col] == var_lb -# all_same_ub &= model.colUpper[var.col] == var_ub -# end -# str_lb = var_lb == -Inf ? "-"*sym[:infty] : str_round(var_lb) -# str_ub = var_ub == +Inf ? sym[:infty] : str_round(var_ub) -# -# # Special case bounds printing based on the category -# if var_cat == :Bin # x in {0,1} -# return "$name_idx $(sym[:in]) $(sym[:open_set])0,1$(sym[:close_set]) $idx_sets" -# elseif var_cat == :SemiInt # x in union of 0 and {lb,...,ub} -# si_lb = all_same_lb ? str_lb : sym[:dots] -# si_ub = all_same_ub ? str_ub : sym[:dots] -# return "$name_idx $(sym[:in]) $(sym[:open_set])$si_lb,$(sym[:dots]),$si_ub$(sym[:close_set]) $(sym[:union]) $(sym[:open_set])0$(sym[:close_set]) $idx_sets" -# elseif var_cat == :SemiCont # x in union of 0 and [lb,ub] -# si_lb = all_same_lb ? str_lb : sym[:dots] -# si_ub = all_same_ub ? str_ub : sym[:dots] -# return "$name_idx $(sym[:in]) $(sym[:open_rng])$si_lb,$si_ub$(sym[:close_rng]) $(sym[:union]) $(sym[:open_set])0$(sym[:close_set]) $idx_sets" -# elseif var_cat == :Fixed -# si_bnd = all_same_lb ? str_lb : sym[:dots] -# return "$name_idx = $si_bnd $idx_sets" -# end -# # Continuous and Integer -# idx_sets = var_cat == :Int ? ", $(sym[:integer]), $idx_sets" : " $idx_sets" -# if all_same_lb && all_same_ub -# # Free variable -# var_lb == -Inf && var_ub == +Inf && return "$name_idx$idx_sets" -# # No lower bound -# var_lb == -Inf && return "$name_idx $(sym[:leq]) $str_ub$idx_sets" -# # No upper bound -# var_ub == +Inf && return "$name_idx $(sym[:geq]) $str_lb$idx_sets" -# # Range -# return "$str_lb $(sym[:leq]) $name_idx $(sym[:leq]) $str_ub$idx_sets" -# end -# if all_same_lb && !all_same_ub -# var_lb == -Inf && return "$name_idx $(sym[:leq]) $(sym[:dots])$idx_sets" -# return "$str_lb $(sym[:leq]) $name_idx $(sym[:leq]) $(sym[:dots])$idx_sets" -# end -# if !all_same_lb && all_same_ub -# var_ub == +Inf && return "$name_idx $(sym[:geq]) $(sym[:dots])$idx_sets" -# return "$(sym[:dots]) $(sym[:leq]) $name_idx $(sym[:leq]) $str_ub$idx_sets" -# end -# return "$(sym[:dots]) $(sym[:leq]) $name_idx $(sym[:leq]) $(sym[:dots])$idx_sets" -# end -# -# # UTILITY FUNCTIONS FOR cont_str -# function cont_str_set(idxset::Union{Range,Array}, dots) # 2:2:20 -> {2,4..18,20} -# length(idxset) == 0 && return dots -# length(idxset) == 1 && return string(idxset[1]) -# length(idxset) == 2 && return string(idxset[1],",",idxset[2]) -# length(idxset) == 3 && return string(idxset[1],",",idxset[2],",",idxset[3]) -# length(idxset) == 4 && return string(idxset[1],",",idxset[2],",",idxset[3],",",idxset[4]) -# length(idxset) == 5 && return string(idxset[1],",",idxset[2],",",idxset[3],",",idxset[4],",",idxset[5]) -# return string(idxset[1],",",idxset[2],",",dots,",",idxset[end-1],",",idxset[end]) -# end -# cont_str_set(idxset, dots) = return dots # Fallback -# # parse_conditions -# # Not exported. Traverses an expression and constructs an array with entries -# # corresponding to each condition. More specifically, if the condition is -# # a && (b || c) && (d && e), it returns [a, b || c, d, e]. -# parse_conditions(not_an_expr) = not_an_expr -# function parse_conditions(expr::Expr) -# ret = Any[] -# if expr.head != :&& -# return push!(ret, expr) -# end -# recurse = map(parse_conditions, expr.args) -# vcat(ret, recurse...) -# end -# -# # Handlers to use correct symbols -# cont_str(::Type{REPLMode}, j; mathmode=false) = -# cont_str(REPLMode, j, repl) -# cont_str(::Type{IJuliaMode}, j; mathmode=true) = -# math(cont_str(IJuliaMode, j, ijulia), mathmode) -# -# #------------------------------------------------------------------------ -# ## JuMPContainer{Float64} -# #------------------------------------------------------------------------ -# Base.show(io::IO, j::JuMPContainer{Float64}) = print(io, val_str(REPLMode,j)) -# function val_str{N}(mode, j::JuMPArray{Float64,N}) -# out_str = "$(getname(j)): $N dimensions:" -# if isempty(j) -# return out_str * "\n (no entries)" -# end -# -# function val_str_rec(depth, parent_index::Vector{Any}, parent_str::AbstractString) -# # Turn index set into strings -# indexset = j.indexsets[depth] -# isa(indexset, AbstractArray) || (indexset = collect(indexset)) -# index_strs = map(string, indexset) -# -# # Determine longest index so we can align columns -# max_index_len = 0 -# for index_str in index_strs -# max_index_len = max(max_index_len, strwidth(index_str)) -# end -# -# # If have recursed, we need to prepend the parent's index strings -# # accumulated, as well as white space so the alignment works. -# for i = 1:length(index_strs) -# index_strs[i] = parent_str * lpad(index_strs[i],max_index_len," ") -# end -# -# # Create a string for the number of spaces we need to indent -# indent = " "^(2*(depth-1)) -# -# # Determine the need to recurse -# if depth == N -# # Deepest level -# for i = 1:length(indexset) -# value = length(parent_index) == 0 ? -# j[indexset[i]] : -# j[parent_index...,indexset[i]] -# out_str *= "\n" * indent * "[" * index_strs[i] * "] = $value" -# end -# else -# # At least one more layer to go -# for i = 1:length(indexset) -# index = indexset[i] -# # Print the ":" version of indices we will recurse over -# out_str *= "\n" * indent * "[" * index_strs[i] * ",:"^(N-depth) * "]" -# val_str_rec(depth+1, -# length(parent_index) == 0 ? Any[index] : Any[parent_index...,index], -# index_strs[i] * ",") -# end -# end -# end -# val_str_rec(1,Any[],"") -# return out_str -# end -# # support types that don't have built-in comparison -# function _isless(t1::Tuple, t2::Tuple) -# n1, n2 = length(t1), length(t2) -# for i = 1:min(n1, n2) -# a, b = t1[i], t2[i] -# if !isequal(a, b) -# return applicable(isless,a,b) ? isless(a, b) : isless(hash(a),hash(b)) -# end -# end -# return n1 < n2 -# end -# function val_str{N}(mode, dict::JuMPDict{Float64,N}) -# nelem = length(dict.tupledict) -# isempty(dict) && return "" -# out_str = "$(getname(dict)): $N dimensions, $nelem " -# out_str *= nelem == 1 ? "entry" : "entries" -# out_str *= ":" -# -# sortedkeys = sort(collect(keys(dict.tupledict)), lt = _isless) -# -# ndim = length(first(keys(dict.tupledict))) -# -# key_strs = Array{String}(length(dict), ndim) -# for (i, key) in enumerate(sortedkeys) -# for j in 1:ndim -# key_strs[i,j] = string(key[j]) -# end -# end -# max_dim_lens = map(1:ndim) do i -# maximum(map(length,key_strs[:,i])) -# end -# key_str = map(1:length(dict)) do i -# join(map(1:ndim) do j -# lpad(key_strs[i,j], max_dim_lens[j]) -# end, ",") -# end -# max_key_len = maximum(map(length,key_str)) -# -# for (i,key) in enumerate(sortedkeys) -# val = dict[key...] -# out_str *= "\n" * lpad("[$(key_str[i])]", max_key_len+3) -# out_str *= " = $val" -# end -# return out_str -# end - - #------------------------------------------------------------------------ ## AffExpr (not GenericAffExpr) #------------------------------------------------------------------------ diff --git a/test/containers.jl b/test/containers.jl new file mode 100644 index 00000000000..38eb95ff52a --- /dev/null +++ b/test/containers.jl @@ -0,0 +1,162 @@ +# 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 http://mozilla.org/MPL/2.0/. + +using JuMP +using Base.Test + +macro dummycontainer(expr, requestedtype) + name = gensym() + refcall, indexvars, indexsets, condition = JuMP.buildrefsets(expr, name) + if condition == :() + return JuMP.generatecontainer(Bool, indexvars, indexsets, requestedtype)[1] + else + if requestedtype != :Auto && requestedtype != :Dict + return :(error("")) + end + return JuMP.generatecontainer(Bool, indexvars, indexsets, :Dict)[1] + end +end + +function containermatches(c1::AbstractArray,c2::AbstractArray) + return typeof(c1) == typeof(c2) && size(c1) == size(c2) +end + +containermatches(c1::Dict, c2::Dict) = (eltype(c1) == eltype(c2)) +containermatches(c1, c2) = false + +@testset "Container syntax" begin + @test containermatches(@dummycontainer([i=1:10], Auto), Vector{Bool}(10)) + @test containermatches(@dummycontainer([i=1:10], Array), Vector{Bool}(10)) + @test containermatches(@dummycontainer([i=1:10], JuMPArray), JuMPArray(Vector{Bool}(10), 1:10)) + @test containermatches(@dummycontainer([i=1:10], Dict), Dict{Any,Bool}()) + + @test containermatches(@dummycontainer([i=1:10,1:2], Auto), Matrix{Bool}(10,2)) + @test containermatches(@dummycontainer([i=1:10,1:2], Array), Matrix{Bool}(10,2)) + @test containermatches(@dummycontainer([i=1:10,n=1:2], JuMPArray), JuMPArray(Matrix{Bool}(10,2), 1:10, 1:2)) + @test containermatches(@dummycontainer([i=1:10,1:2], Dict), Dict{Any,Bool}()) + + @test containermatches(@dummycontainer([i=1:10,n=2:3], Auto), JuMPArray(Matrix{Bool}(10,2), 1:10, 2:3)) + @test_throws ErrorException @dummycontainer([i=1:10,2:3], Array) + @test containermatches(@dummycontainer([i=1:10,n=2:3], JuMPArray), JuMPArray(Matrix{Bool}(10,2), 1:10, 2:3)) + @test containermatches(@dummycontainer([i=1:10,n=2:3], Dict), Dict{Any,Bool}()) + + + S = Base.OneTo(10) + @test containermatches(@dummycontainer([i=S], Auto), Vector{Bool}(10)) + @test containermatches(@dummycontainer([i=S], Array), Vector{Bool}(10)) + @test containermatches(@dummycontainer([i=S], JuMPArray), JuMPArray(Vector{Bool}(10), S)) + @test containermatches(@dummycontainer([i=S], Dict), Dict{Any,Bool}()) + + @test containermatches(@dummycontainer([i=S,1:2], Auto), Matrix{Bool}(10,2)) + @test containermatches(@dummycontainer([i=S,1:2], Array), Matrix{Bool}(10,2)) + @test containermatches(@dummycontainer([i=S,n=1:2], JuMPArray), JuMPArray(Matrix{Bool}(10,2), S, 1:2)) + @test containermatches(@dummycontainer([i=S,1:2], Dict), Dict{Any,Bool}()) + + S = 1:10 + # Not type stable to return an Array by default even when S is one-based interval + @test containermatches(@dummycontainer([i=S], Auto), JuMPArray(Vector{Bool}(10), S)) + @test containermatches(@dummycontainer([i=S], Array), Vector{Bool}(10)) + @test containermatches(@dummycontainer([i=S], JuMPArray), JuMPArray(Vector{Bool}(10), S)) + @test containermatches(@dummycontainer([i=S], Dict), Dict{Any,Bool}()) + + @test containermatches(@dummycontainer([i=S,n=1:2], Auto), JuMPArray(Matrix{Bool}(10,2), S, 1:2)) + @test containermatches(@dummycontainer([i=S,1:2], Array), Matrix{Bool}(10,2)) + @test containermatches(@dummycontainer([i=S,n=1:2], JuMPArray), JuMPArray(Matrix{Bool}(10,2), S, 1:2)) + @test containermatches(@dummycontainer([i=S,1:2], Dict), Dict{Any,Bool}()) + + # TODO: test case where S is index set not supported by JuMPArrays (does this exist?) + + # Conditions + @test containermatches(@dummycontainer([i=1:10; iseven(i)], Auto), Dict{Any,Bool}()) + @test_throws ErrorException @dummycontainer([i=1:10; iseven(i)], Array) + @test_throws ErrorException @dummycontainer([i=1:10; iseven(i)], JuMPArray) + @test containermatches(@dummycontainer([i=1:10; iseven(i)], Dict), Dict{Any,Bool}()) + + # Dependent indices + @test containermatches(@dummycontainer([i=1:10, j=1:i], Auto), Dict{Any,Bool}()) + @test_throws ErrorException @dummycontainer([i=1:10, j=1:i], Array) + @test_throws ErrorException @dummycontainer([i=1:10, j=1:i], JuMPArray) + @test containermatches(@dummycontainer([i=1:10, j=1:i], Dict), Dict{Any,Bool}()) + +end + +@testset "JuMPArray" begin + s1 = 2:3 + s2 = [:a,:b] + plus1(x) = x + 1 + + A = JuMPArray([1.0,2.0], s1) + @test A[2] == 1.0 + @test A[3] == 2.0 + @test length(A) == 2 + @test size(A) == (2,) + B = plus1.(A) + @test B[2] == 2.0 + @test B[3] == 3.0 + @test sprint(show, B) == """ +1-dimensional JuMPArray{Float64,1,...} with index sets: + Dimension 1, 2:3 +And data, a 2-element Array{Float64,1}: + 2.0 + 3.0""" + + A = JuMPArray([1.0,2.0], s2) + @test A[:a] == 1.0 + @test A[:b] == 2.0 + @test length(A) == 2 + @test size(A) == (2,) + B = plus1.(A) + @test B[:a] == 2.0 + @test B[:b] == 3.0 + @test sprint(show, B) == """ +1-dimensional JuMPArray{Float64,1,...} with index sets: + Dimension 1, Symbol[:a, :b] +And data, a 2-element Array{Float64,1}: + 2.0 + 3.0""" + + A = JuMPArray([1 2; 3 4], s1, s2) + @test size(A) == (2,2) + @test A[2,:a] == 1 + @test A[3,:a] == 3 + @test A[2,:b] == 2 + @test A[3,:b] == 4 + @test A[:,:a] == JuMPArray([1,3], s1) + @test A[2, :] == JuMPArray([1,2], s2) + @test sprint(show, A) == """ +2-dimensional JuMPArray{Int64,2,...} with index sets: + Dimension 1, 2:3 + Dimension 2, Symbol[:a, :b] +And data, a 2×2 Array{Int64,2}: + 1 2 + 3 4""" + + A = JuMPArray(zeros(2,2,2,2), 2:3, [:a, :b], -1:0, ["a","b"]) + A[2,:a,-1,"a"] = 1.0 + @test A[:,:,-1,"a"] == JuMPArray([1.0 0.0; 0.0 0.0], 2:3, [:a,:b]) + @test_throws KeyError A[2,:a,-1,:a] + @test sprint(show, A) == """ +4-dimensional JuMPArray{Float64,4,...} with index sets: + Dimension 1, 2:3 + Dimension 2, Symbol[:a, :b] + Dimension 3, -1:0 + Dimension 4, String["a", "b"] +And data, a 2×2×2×2 Array{Float64,4}: +[:, :, -1, "a"] = + 1.0 0.0 + 0.0 0.0 + +[:, :, 0, "a"] = + 0.0 0.0 + 0.0 0.0 + +[:, :, -1, "b"] = + 0.0 0.0 + 0.0 0.0 + +[:, :, 0, "b"] = + 0.0 0.0 + 0.0 0.0""" +end diff --git a/test/runtests.jl b/test/runtests.jl index e5d28b59c00..9492eb58d86 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,6 +24,7 @@ using CSDP # Static tests - most don't require a solver #include("print.jl") +include("containers.jl") include("variable.jl") include("objective.jl") include("constraint.jl") diff --git a/test/variable.jl b/test/variable.jl index 54491f49f53..df00ef1b588 100644 --- a/test/variable.jl +++ b/test/variable.jl @@ -72,6 +72,11 @@ using Base.Test @test typeof(zero(nobounds)) == AffExpr @test typeof(one(nobounds)) == AffExpr + + @test_throws ErrorException @variable(mcon, [(0,0)]) # #922 + x = @variable(mcon, [(0,2)]) + @test JuMP.name(x[0]) == "__anon__[0]" + @test JuMP.name(x[2]) == "__anon__[2]" end @testset "get and set bounds" begin @@ -131,8 +136,23 @@ using Base.Test @testset "repeated elements in index set (issue #199)" begin repeatmod = Model() s = [:x,:x,:y] - @variable(repeatmod, x[s]) - @test JuMP.numvar(repeatmod) == 3 + @test_throws ErrorException @variable(repeatmod, x[s], container = JuMPArray) + @test_throws ErrorException @variable(repeatmod, x[s], container = Dict) + @test_throws ErrorException @variable(repeatmod, x[s,[1]], container = JuMPArray) + @test_throws ErrorException @variable(repeatmod, x[s,[1]], container = Dict) + end + + @testset "Base.OneTo as index set (#933)" begin + m = Model() + x = @variable(m, [Base.OneTo(3), 1:2], container=Auto) + @test x isa Matrix{Variable} + @test size(x) == (3,2) + x = @variable(m, [Base.OneTo(3), 1:2], container=Array) + @test x isa Matrix{Variable} + @test size(x) == (3,2) + x = @variable(m, [Base.OneTo(3), 1:2], container=JuMPArray) + @test x isa JuMPArray{Variable} + @test size(x) == (3,2) end # TODO reenable when printing comes back @@ -201,67 +221,68 @@ end @testset "Slices of JuMPArray (#684)" begin m = Model() - @variable(m, x[1:3, 1:4,1:2]) + @variable(m, x[1:3, 1:4,1:2], container=JuMPArray) @variable(m, y[1:3,-1:2,3:4]) @variable(m, z[1:3,-1:2:4,3:4]) @variable(m, w[1:3,-1:2,[:red,"blue"]]) - @test x[:] == vec(sliceof(x, 1:3, 1:4, 1:2)) - @test x[:,:,:] == sliceof(x, 1:3, 1:4, 1:2) - @test x[1,:,:] == sliceof(x, 1, 1:4, 1:2) - @test x[1,:,2] == sliceof(x, 1, 1:4, 2) - @test_throws BoundsError x[1,:,3] - @test x[1:2,:,:] == sliceof(x, 1:2, 1:4, 1:2) - @test x[1:2,:,2] == sliceof(x, 1:2, 1:4, 2) - @test x[1:2,:,1:2] == sliceof(x, 1:2, 1:4, 1:2) - @test_throws BoundsError x[1:2,:,1:3] - - @test y[:] == vec(sliceof(y, 1:3, -1:2, 3:4)) - @test y[:,:,:] == sliceof(y, 1:3, -1:2, 3:4) - @test y[1,:,:] == sliceof(y, 1, -1:2, 3:4) - @test y[1,:,4] == sliceof(y, 1, -1:2, 4) - @test_throws ErrorException y[1,:,5] - @test y[1:2,:,:] == sliceof(y, 1:2, -1:2, 3:4) - @test y[1:2,:,4] == sliceof(y, 1:2, -1:2, 4) - @test y[1:2,:,3:4] == sliceof(y, 1:2, -1:2, 3:4) - @test_throws BoundsError y[1:2,:,1:3] - - @test z[:] == vec(sliceof(z, 1:3, -1:2:4, 3:4)) - @test z[:,1,:] == sliceof(z, 1:3, 1, 3:4) - @test z[1,1,:] == sliceof(z, 1, 1, 3:4) - @test_throws ErrorException z[:,5,3] - @test z[1:2,1,:] == sliceof(z, 1:2, 1, 3:4) - @test z[1:2,1,4] == sliceof(z, 1:2, 1, 4) - @test z[1:2,1,3:4] == sliceof(z, 1:2, 1, 3:4) - @test_throws BoundsError z[1:2,1,1:3] - - @test w[:] == vec(sliceof(w, 1:3, -1:2, [:red,"blue"])) - @test_throws ErrorException w[:,:,:] - @test w[1,:,"blue"] == sliceof(w, 1, -1:2, ["blue"]) - @test w[1,:,:red] == sliceof(w, 1, -1:2, [:red]) - @test_throws ErrorException w[1,:,"green"] - @test w[1:2,:,"blue"] == sliceof(w, 1:2, -1:2, ["blue"]) - @test_throws ErrorException w[1:2,:,[:red,"blue"]] + #@test x[:] == vec(sliceof(x, 1:3, 1:4, 1:2)) + @test x isa JuMPArray + @test x[:,:,:].data == sliceof(x, 1:3, 1:4, 1:2) + @test x[1,:,:].data == sliceof(x, 1, 1:4, 1:2) + @test x[1,:,2].data == sliceof(x, 1, 1:4, 2) + @test_throws KeyError x[1,:,3] + #@test x[1:2,:,:].data == sliceof(x, 1:2, 1:4, 1:2) + #@test x[1:2,:,2].data == sliceof(x, 1:2, 1:4, 2) + #@test x[1:2,:,1:2].data == sliceof(x, 1:2, 1:4, 1:2) + @test_throws KeyError x[1:2,:,1:3] + + #@test y[:] == vec(sliceof(y, 1:3, -1:2, 3:4)) + @test y[:,:,:].data == sliceof(y, 1:3, -1:2, 3:4) + @test y[1,:,:].data == sliceof(y, 1, -1:2, 3:4) + @test y[1,:,4].data == sliceof(y, 1, -1:2, 4) + @test_throws KeyError y[1,:,5] + # @test y[1:2,:,:] == sliceof(y, 1:2, -1:2, 3:4) + # @test y[1:2,:,4] == sliceof(y, 1:2, -1:2, 4) + # @test y[1:2,:,3:4] == sliceof(y, 1:2, -1:2, 3:4) + # @test_throws BoundsError y[1:2,:,1:3] + + #@test z[:] == vec(sliceof(z, 1:3, -1:2:4, 3:4)) + @test z[:,1,:].data == sliceof(z, 1:3, 1, 3:4) + @test z[1,1,:].data == sliceof(z, 1, 1, 3:4) + @test_throws KeyError z[:,5,3] + # @test z[1:2,1,:] == sliceof(z, 1:2, 1, 3:4) + # @test z[1:2,1,4] == sliceof(z, 1:2, 1, 4) + # @test z[1:2,1,3:4] == sliceof(z, 1:2, 1, 3:4) + # @test_throws BoundsError z[1:2,1,1:3] + + #@test w[:] == vec(sliceof(w, 1:3, -1:2, [:red,"blue"])) + @test w[:,:,:] == w + @test w[1,:,"blue"].data == sliceof(w, 1, -1:2, ["blue"]) + @test w[1,:,:red].data == sliceof(w, 1, -1:2, [:red]) + @test_throws KeyError w[1,:,"green"] + # @test w[1:2,:,"blue"] == sliceof(w, 1:2, -1:2, ["blue"]) + # @test_throws ErrorException w[1:2,:,[:red,"blue"]] end - @testset "Can't use end for indexing a JuMPContainer" begin - m = Model() - @variable(m, x[0:2,1:4]) - @variable(m, y[i=1:4,j=1:4;true]) - @variable(m, z[0:2]) - @test_throws ErrorException x[end,1] - @test_throws ErrorException x[end-1] - @test_throws ErrorException x[0,end-1] - @test_throws MethodError y[end,end-1] - @test_throws MethodError y[end,1] - @test_throws ErrorException z[end] - end + # @testset "Can't use end for indexing a JuMPContainer" begin + # m = Model() + # @variable(m, x[0:2,1:4]) + # @variable(m, y[i=1:4,j=1:4;true]) + # @variable(m, z[0:2]) + # @test_throws ErrorException x[end,1] + # @test_throws ErrorException x[end-1] + # @test_throws ErrorException x[0,end-1] + # @test_throws MethodError y[end,end-1] + # @test_throws MethodError y[end,1] + # @test_throws ErrorException z[end] + # end @testset "Unsigned dimension lengths" begin m = Model() t = UInt(4) @variable(m, x[1:t]) - @constraintref(y[1:t]) + #@constraintref(y[1:t]) @test JuMP.numvar(m) == 4 end