Skip to content

Commit

Permalink
Merge pull request jump-dev#1328 from JuliaOpt/bl/parsevar
Browse files Browse the repository at this point in the history
Add parsevariable
  • Loading branch information
blegat authored Jun 13, 2018
2 parents c0b667e + beee8cf commit 5783fcb
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 105 deletions.
114 changes: 69 additions & 45 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,61 @@ function namecall(basename, idxvars)
return ex
end

reverse_sense(::Val{:<=}) = :>=
reverse_sense(::Val{:≤}) = :
reverse_sense(::Val{:>=}) = :<=
reverse_sense(::Val{:≥}) = :
reverse_sense(::Val{:(==)}) = :(==)

"""
parse_one_operator_variable(_error::Function, infoexpr::VariableInfoExpr, sense::Val{S}, value) where S
Update `infoexr` for a variable expression in the `@variable` macro of the form `variable name S value`.
"""
parse_one_operator_variable(_error::Function, infoexpr::VariableInfoExpr, ::Union{Val{:<=}, Val{:≤}}, upper) = setupperbound_or_error(_error, infoexpr, upper)
parse_one_operator_variable(_error::Function, infoexpr::VariableInfoExpr, ::Union{Val{:>=}, Val{:≥}}, lower) = setlowerbound_or_error(_error, infoexpr, lower)
parse_one_operator_variable(_error::Function, infoexpr::VariableInfoExpr, ::Val{:(==)}, value) = fix_or_error(_error, infoexpr, value)
parse_one_operator_variable(_error::Function, infoexpr::VariableInfoExpr, ::Val{S}, value) where S = _error("Unknown sense $S.")
function parsevariable(_error::Function, infoexpr::VariableInfoExpr, sense::Symbol, var, value)
# Variable declaration of the form: var sense value

# There is not way to determine at parsing time which of lhs or rhs is the
# variable name and which is the value. For instance, lhs could be the
# Symbol `:x` and rhs could be the Symbol `:a` where a variable `a` is
# assigned to 1 in the local scope. Knowing this, we know that `x` is the
# variable name but at parse time there is now way to know that `a` has
# a value.
# Therefore, we always assume that the variable is the `lhs` and throw
# an helpful error in the the case were we can easily determine that the
# user placed the variable in the rhs, i.e. the case where the rhs is a
# constant number.
var isa Number && _error("Variable declaration of the form `$var $S $value` is not supported. Use `$value $(reverse_sense(sense)) $var` instead.")
parse_one_operator_variable(_error, infoexpr, Val(sense), esc_nonconstant(value))
var
end

function parseternaryvariable(_error::Function, infoexpr::VariableInfoExpr,
::Union{Val{:<=}, Val{:≤}}, lower,
::Union{Val{:<=}, Val{:≤}}, upper)
setlowerbound_or_error(_error, infoexpr, lower)
setupperbound_or_error(_error, infoexpr, upper)
end
function parseternaryvariable(_error::Function, infoexpr::VariableInfoExpr,
::Union{Val{:>=}, Val{:≥}}, upper,
::Union{Val{:>=}, Val{:≥}}, lower)
parseternaryvariable(_error, infoexpr, Val(:), lower, Val(:), upper)
end
function parseternaryvariable(_error::Function, infoexpr::VariableInfoExpr,
::Val, lvalue,
::Val, rvalue)
_error("Use the form lb <= ... <= ub.")
end
function parsevariable(_error::Function, infoexpr::VariableInfoExpr, lvalue, lsign::Symbol, var, rsign::Symbol, rvalue)
# lvalue lsign var rsign rvalue
parseternaryvariable(_error, infoexpr, Val(lsign), esc_nonconstant(lvalue), Val(rsign), esc_nonconstant(rvalue))
var
end

# @variable(m, expr, extra...; kwargs...)
# where `extra` is a list of extra positional arguments and `kwargs` is a list of keyword arguments.
#
Expand Down Expand Up @@ -1002,51 +1057,20 @@ macro variable(args...)
extra_kwargs = filter(kw -> kw.args[1] != :basename && !isinfokeyword(kw), kwargs)
basename_kwargs = filter(kw -> kw.args[1] == :basename, kwargs)
infoexpr = VariableInfoExpr(; keywordify.(info_kwargs)...)
var = x
# Identify the variable bounds. Five (legal) possibilities are "x >= lb",
# "x <= ub", "lb <= x <= ub", "x == val", or just plain "x"
explicit_comparison = false
if isexpr(x,:comparison) # two-sided
explicit_comparison = true
if x.args[2] == :>= || x.args[2] == :
# ub >= x >= lb
x.args[4] == :>= || x.args[4] == : || _error("Invalid variable bounds")
var = x.args[3]
setlowerbound_or_error(_error, infoexpr, esc_nonconstant(x.args[5]))
setupperbound_or_error(_error, infoexpr, esc_nonconstant(x.args[1]))
elseif x.args[2] == :<= || x.args[2] == :
# lb <= x <= u
var = x.args[3]
(x.args[4] != :<= && x.args[4] != :) &&
_error("Expected <= operator after variable name.")
setlowerbound_or_error(_error, infoexpr, esc_nonconstant(x.args[1]))
setupperbound_or_error(_error, infoexpr, esc_nonconstant(x.args[5]))
else
_error("Use the form lb <= ... <= ub.")
end
elseif isexpr(x,:call)
explicit_comparison = true
if x.args[1] == :>= || x.args[1] == :
# x >= lb
var = x.args[2]
var isa Number && _error("Variable declaration of the form `$(x.args[3]) $(x.args[1]) $var` is not supported. Use `$(x.args[3]) <= $var` instead.")
@assert length(x.args) == 3
setlowerbound_or_error(_error, infoexpr, esc_nonconstant(x.args[3]))
elseif x.args[1] == :<= || x.args[1] == :
# x <= ub
var = x.args[2]
var isa Number && _error("Variable declaration of the form `$(x.args[3]) $(x.args[1]) $var` is not supported. Use `$(x.args[3]) >= $var` instead.")
@assert length(x.args) == 3
setupperbound_or_error(_error, infoexpr, esc_nonconstant(x.args[3]))
elseif x.args[1] == :(==)
# fixed variable
var = x.args[2]
@assert length(x.args) == 3
fix_or_error(_error, infoexpr, esc(x.args[3]))
else
# Its a comparsion, but not using <= ... <=
_error("Unexpected syntax $(string(x)).")
end

# There are four cases to consider:
# x | type of x | x.head
# ----------------------------------------+-----------+------------
# var | Symbol | NA
# var[1:2] | Expr | :ref
# var <= ub or var[1:2] <= ub | Expr | :call
# lb <= var <= ub or lb <= var[1:2] <= ub | Expr | :comparison
# In the two last cases, we call parsevariable
explicit_comparison = isexpr(x, :comparison) || isexpr(x, :call)
if explicit_comparison
var = parsevariable(_error, infoexpr, x.args...)
else
var = x
end

anonvar = isexpr(var, :vect) || isexpr(var, :vcat) || anon_singleton
Expand Down
147 changes: 87 additions & 60 deletions test/variable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,66 +19,93 @@ using Compat.Test
@testset "constructors" begin
# Constructors
mcon = Model()
@variable(mcon, nobounds)
@test !JuMP.haslowerbound(nobounds)
@test !JuMP.hasupperbound(nobounds)
@test !JuMP.isfixed(nobounds)
@test JuMP.name(nobounds) == "nobounds"

@variable(mcon, lbonly >= 0, Bin)
@test JuMP.haslowerbound(lbonly)
@test JuMP.lowerbound(lbonly) == 0.0
@test !JuMP.hasupperbound(lbonly)
@test !JuMP.isfixed(lbonly)
@test JuMP.isbinary(lbonly)
@test !JuMP.isinteger(lbonly)

@variable(mcon, ubonly <= 1, Int)
@test !JuMP.haslowerbound(ubonly)
@test JuMP.hasupperbound(ubonly)
@test JuMP.upperbound(ubonly) == 1.0
@test !JuMP.isfixed(ubonly)
@test !JuMP.isbinary(ubonly)
@test JuMP.isinteger(ubonly)

@variable(mcon, 0 <= bothb <= 1)
@test JuMP.haslowerbound(bothb)
@test JuMP.lowerbound(bothb) == 0.0
@test JuMP.hasupperbound(bothb)
@test JuMP.upperbound(bothb) == 1.0
@test !JuMP.isfixed(bothb)

@variable(mcon, fixed == 1.0)
@test !JuMP.haslowerbound(fixed)
@test !JuMP.hasupperbound(fixed)
@test JuMP.isfixed(fixed)
@test JuMP.fixvalue(fixed) == 1.0

@variable(mcon, onerangeub[-7:1] <= 10, Int)
@variable(mcon, manyrangelb[0:1,10:20,1:1] >= 2)
@test JuMP.haslowerbound(manyrangelb[0,15,1])
@test JuMP.lowerbound(manyrangelb[0,15,1]) == 2
@test !JuMP.hasupperbound(manyrangelb[0,15,1])

s = ["Green","Blue"]
@variable(mcon, x[i=-10:10,s] <= 5.5, Int, start=i+1)
@test JuMP.upperbound(x[-4,"Green"]) == 5.5
@test JuMP.name(x[-10,"Green"]) == "x[-10,Green]"
# TODO: broken because of https://github.com/JuliaOpt/MathOptInterface.jl/issues/302
#@test JuMP.startvalue(x[-3,"Blue"]) == -2
@test isequal(mcon[:lbonly],lbonly)
@test isequal(mcon[:ubonly],ubonly)
@test isequal(mcon[:onerangeub][-7],onerangeub[-7])
@test_throws ErrorException @variable(mcon, lbonly)
@test_throws KeyError mcon[:foo]

@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]) == ""
@test JuMP.name(x[2]) == ""

@testset "No bound" begin
@variable(mcon, nobounds)
@test !JuMP.haslowerbound(nobounds)
@test !JuMP.hasupperbound(nobounds)
@test !JuMP.isfixed(nobounds)
@test JuMP.name(nobounds) == "nobounds"

@test typeof(zero(nobounds)) == AffExpr
@test typeof(one(nobounds)) == AffExpr
end

@testset "Lower bound" begin
@variable(mcon, lbonly >= 0, Bin)
@test JuMP.haslowerbound(lbonly)
@test JuMP.lowerbound(lbonly) == 0.0
@test !JuMP.hasupperbound(lbonly)
@test !JuMP.isfixed(lbonly)
@test JuMP.isbinary(lbonly)
@test !JuMP.isinteger(lbonly)
@test isequal(mcon[:lbonly],lbonly)
@test_throws ErrorException @variable(mcon, lbonly)
end

@testset "Upper bound" begin
@variable(mcon, ubonly <= 1, Int)
@test !JuMP.haslowerbound(ubonly)
@test JuMP.hasupperbound(ubonly)
@test JuMP.upperbound(ubonly) == 1.0
@test !JuMP.isfixed(ubonly)
@test !JuMP.isbinary(ubonly)
@test JuMP.isinteger(ubonly)
@test isequal(mcon[:ubonly],ubonly)
end

@testset "Interval" begin
function has_bounds(var, lb, ub)
@test JuMP.haslowerbound(var)
@test JuMP.lowerbound(var) == lb
@test JuMP.hasupperbound(var)
@test JuMP.upperbound(var) == ub
@test !JuMP.isfixed(var)
end

@variable(mcon, 0 <= bothb1 <= 1)
has_bounds(bothb1, 0.0, 1.0)
@variable(mcon, 0 bothb2 1)
has_bounds(bothb2, 0.0, 1.0)
@variable(mcon, 1 >= bothb3 >= 0)
has_bounds(bothb3, 0.0, 1.0)
@variable(mcon, 1 bothb4 0)
has_bounds(bothb4, 0.0, 1.0)
@test_macro_throws ErrorException @variable(mcon, 1 bothb5 0)
@test_macro_throws ErrorException @variable(mcon, 1 bothb6 0)
end

@testset "Fix" begin
@variable(mcon, fixed == 1.0)
@test !JuMP.haslowerbound(fixed)
@test !JuMP.hasupperbound(fixed)
@test JuMP.isfixed(fixed)
@test JuMP.fixvalue(fixed) == 1.0
end

@testset "Custom index sets" begin
@variable(mcon, onerangeub[-7:1] <= 10, Int)
@variable(mcon, manyrangelb[0:1,10:20,1:1] >= 2)
@test JuMP.haslowerbound(manyrangelb[0,15,1])
@test JuMP.lowerbound(manyrangelb[0,15,1]) == 2
@test !JuMP.hasupperbound(manyrangelb[0,15,1])

s = ["Green","Blue"]
@variable(mcon, x[i=-10:10,s] <= 5.5, Int, start=i+1)
@test JuMP.upperbound(x[-4,"Green"]) == 5.5
@test JuMP.name(x[-10,"Green"]) == "x[-10,Green]"
# TODO: broken because of https://github.com/JuliaOpt/MathOptInterface.jl/issues/302
#@test JuMP.startvalue(x[-3,"Blue"]) == -2
@test isequal(mcon[:onerangeub][-7],onerangeub[-7])
@test_throws KeyError mcon[:foo]
end

@testset "Anonymous variable" begin
@test_throws ErrorException @variable(mcon, [(0,0)]) # #922
x = @variable(mcon, [(0,2)])
@test JuMP.name(x[0]) == ""
@test JuMP.name(x[2]) == ""
end
end

@testset "isvalid and delete variable" begin
Expand Down

0 comments on commit 5783fcb

Please sign in to comment.