Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

syntax brainstorming #692

Closed
mlubin opened this issue Mar 5, 2016 · 87 comments
Closed

syntax brainstorming #692

mlubin opened this issue Mar 5, 2016 · 87 comments

Comments

@mlubin
Copy link
Member

mlubin commented Mar 5, 2016

Addressing the issue raised in #688, the current syntax of @defVar, despite the name, doesn't make it clear that all we're doing is defining a julia variable. Maybe it's time to brainstorm a new approach:

Both examples below are valid julia syntax:
Idea 1:

@variables m begin
    x = (0 <= Variable[i=1:n](Int, start=5) <= i)
    ...
end

The trouble here is that it looks like you're assigning x to refer to the constraints enforcing the bounds instead of the variables.

Idea 2:

# variant 1
@variables m begin
    x = Variable[i=1:n](Int, start=5,lowerbound=0,upperbound=i)
    ...
end
# variant 2
@variables m begin
    x[i=1:n] = Variable(Int, start=5,lowerbound=0,upperbound=i)
    ...
end

We lose some of the magic of using comparison operators, but maybe this is much clearer. You can always use explicit constraints instead if you prefer the mathy syntax. Of the two variants I prefer the first because it's more clear that we're just assigning a collection of variables to x whereas the second variant might lead you to think that you can append indices to x later on.

CC @StefanKarpinski @carnaval @yeesian @JackDunnNZ @davidanthoff @juan-pablo-vielma @chriscoey

@davidanthoff
Copy link
Contributor

I think I like idea 2 variant 1 best. Idea 1 seems too busy, and the syntax too weird, to me.

If you go with the x = Variable approach, it might be worth thinking about renaming the macro to something like @jump, and then also allow lines that read like y = Parameter(), and maybe even constraints like that? It seems weird to have to use a macro called variable, and then to specify again later that I am adding variables...

@mlubin
Copy link
Member Author

mlubin commented Mar 7, 2016

Yes, a single @jump macro is a natural conclusion from idea 2.

@chriscoey
Copy link
Contributor

chriscoey commented Mar 7, 2016

I agree with David on idea 2 variant 1.

I also never liked the LB <= var[...] <= UB syntax in defvar- it messes up vertical alignment (which helps when quickly glancing over a model) and doesn't feel like a natural way to code.

@joehuchette
Copy link
Contributor

I prefer idea 2 variant 2 the most of the options presented, but still find it less satisfying than the current syntax. We lose the bounds for syntax which I like very much, and although the assignment is a bit clearer with the =, it's still unlike any other assignment you would be able to write in julia (without macros).

@IainNZ
Copy link
Collaborator

IainNZ commented Mar 7, 2016

Currently

@defVar(m, buy[1:n] >= 0, Int)
@defVar(m, 0 <= send[i=1:n,j=1:n] <= capacity[i,j])
@defVar(m, 0 <= sold[i=1:n] <= demand[i])

Idea 1

@variables m begin
    buy = (Variable[i=1:n](Int) >= 0)
    send = (0 <= Variable[i=1:n,j=1:n] <= capacity[i,j]))
    sold = (0 <= Variable[i=1:n] <= demand[i])
end

Idea 1 Var. 1

@jump m begin
    buy = Variable[i=1:n](Int, lowerbound=0)
    send = Variable[i=1:n,j=1:n](lowerbound=0,upperbound=capacity[i,j])
    sold = Variable[i=1:n](Int, lowerbound=0,upperbound=demand[i])
end

Idea 1 Var. 2

@jump m begin
    buy[i=1:n] = Variable(Int, lowerbound=0)
    send[i=1:n,j=1:n] = Variable(lowerbound=0,upperbound=capacity[i,j])
    sold[i=1:n] = Variable(Int, lowerbound=0,upperbound=demand[i])
end

I don't really find any of them an improvement to be honest.

@IainNZ
Copy link
Collaborator

IainNZ commented Mar 7, 2016

acpower

@defVar(mod, bus_voltage_min[bus[i].bustype] <= bus_voltage[i=1:nbus] <= bus_voltage_max[bus[i].bustype], start=1)
@defVar(mod, bus[i].b_shunt_min <= bus_b_shunt[i=1:nbus] <= bus[i].b_shunt_max, start=bus[i].b_shunt0)

or

@defVar(mod, bus_voltage_min[bus[i].bustype] <= 
             bus_voltage[i=1:nbus] <=
             bus_voltage_max[bus[i].bustype], start=1)

Idea 1 Var 2, formatted nicely

@jump b begin
  bus_voltage[i=1:nbus] = Variable(start=1,
                                   lowerbound=bus_voltage_min[bus[i].bustype],
                                   upperbound=bus_voltage_max[bus[i].bustype])
  bus_b_shunt[i=1:nbus] = Variable(start=bus[i].b_shunt0,
                                   lowerbound=bus[i].b_shunt_min,
                                   upperbound=bus[i].b_shunt_max)
end

@IainNZ
Copy link
Collaborator

IainNZ commented Mar 7, 2016

That could be arguably better?

@mlubin
Copy link
Member Author

mlubin commented Mar 7, 2016

Definitely a lot clearer without any context what is happening in the statement.

@IainNZ
Copy link
Collaborator

IainNZ commented Mar 7, 2016

Variant 2 of Idea 2 definitely will still have the "extension" problem, but Variant 1 will not I think.

@mlubin
Copy link
Member Author

mlubin commented Mar 7, 2016

Yes, I prefer variant 1

@chriscoey
Copy link
Contributor

chriscoey commented Mar 7, 2016

Agreed that it's better in Iain's example. But again I think in that example it's the LB <= var <= UB syntax that makes it hard to read in @defvar. That would be fixed by
@defvar(m, varname[i=1:n], Int, >= LB[i], <= UB[i], start = 1)
or
@defvar(m, varname[i=1:n], Int, lb = LB[i], ub = UB[i], start = 1)

Of the variants, I might actually prefer 2 now, because I think you want to be able to see quickly both the variable name and its indexing, and they're nicely aligned on the left before = Variable().

@mlubin
Copy link
Member Author

mlubin commented Mar 7, 2016

The syntax

@defVar(m, varname[i=1:n], Int, >= LB[i], <= UB[i], start = 1)

isn't available to us, would have to be

@defVar(m, varname[i=1:n], Int, lb = LB[i], ub = UB[i], start = 1)

@yeesian
Copy link
Contributor

yeesian commented Mar 7, 2016

Echoing joey: I also really like the bounds syntax of at-defVar for most problems that I write.

If you insist, you can align names using whitespace, e.g.

@defVar(m, x[i=1:p.nlocations] >= 0, Int, start=warmstart[i])
@defVar(m, d[1:nregions]       >= 0, Int)

@defVar(m, 0          <= p[1:nregions]   <= ub)
@defVar(m, lowerbound <= q[1:nlocations] <= upperbound)

it's also why I used to write two-sided bounds the other way round, i.e.

@defVar(m,               x[i=1:nlocations] >= 0, Int, start=warmstart[i])
@defVar(m,               d[1:nregions]     >= 0, Int)
@defVar(m,         ub >= p[1:nregions]     >= 0)
@defVar(m, upperbound >= q[1:nlocations]   >= lowerbound)

For complicated formulations, I find the proposals minor improvements over explicit constructions of JuMP Variables, and have not had as much opportunities to remember to use them.

@ns-1m
Copy link

ns-1m commented Mar 8, 2016

I like Idea 1 Var. 2

@jump m begin
    buy[i=1:n] = Variable(Int, lowerbound=0)
    send[i=1:n,j=1:n] = Variable(lowerbound=0,upperbound=capacity[i,j])
    sold[i=1:n] = Variable(Int, lowerbound=0,upperbound=demand[i])
end

@diam
Copy link

diam commented Mar 10, 2016

I'd like a common prefix for all macro like @jump (or even @jp):

@jump m begin
       send[i=1:n,j=1:n] = Variable(something complicated, but we already now
                                                    that send is an array)
       send[i=1:n,j=1:n] = Variable(lowerbound=0,upperbound=capacity[i,j])
       ... (others variables)
       c_kirchoff[i=1:n] = Contraint(...)
       ... (other constrants_
end
# later we define one more variable with a possibly newer syntaxe that @defVar:
@jumpVar(m, ...)
# or one more constraint
@jumpCst(m, ...)
@jumpSolve(...)
...

-- Maurice

@mlubin
Copy link
Member Author

mlubin commented Mar 15, 2016

There doesn't seem to be too much consensus here, so I don't see any big changes happening soon. The next step forward to proceed with this would be for someone to put together a prototype JuMP extension to test out the new syntax.

@IainNZ
Copy link
Collaborator

IainNZ commented Mar 15, 2016

I think it needs to be tried out for some nontrivial problems/index sets to really capture the impact. Hopefully the things I added here capture at least some use cases

@mlubin
Copy link
Member Author

mlubin commented Sep 29, 2016

Another point here is that we're removing the semicolon from the sum/prod/norm syntax, so now using semicolons to define conditions on variable indices makes very little sense. We might think about moving to a variable declaration syntax more consistent with the generator syntax, e.g,

@variable(m, x[i,j] for i in X, j in Y if i <= j)

@joehuchette
Copy link
Contributor

Does that parse reasonably?

@mlubin
Copy link
Member Author

mlubin commented Sep 29, 2016

@joehuchette, yes, the second argument to the macro is a generator statement.

@juan-pablo-vielma
Copy link

I like it. Some comments:

  1. I imagine X/Y can be something like [1:10]. Any restrictions on what X/Y can be?
  2. Do we want to keep the x[1:n,1:m] syntax too?
  3. How would the detection of array indexing v/s general indexing work? If it is problematic, perhaps using the old syntax in point 2. can signal array indexing

@mlubin
Copy link
Member Author

mlubin commented Sep 30, 2016

I imagine X/Y can be something like [1:10]. Any restrictions on what X/Y can be?

Anything you can loop over in Julia.

Do we want to keep the x[1:n,1:m] syntax too?

I wouldn't want to keep multiple inconsistent syntaxes around. If we make a change like this we should be convinced that it's obviously better than the x[1:n,1:m] so we shouldn't feel bad dropping that syntax.

@mlubin
Copy link
Member Author

mlubin commented Sep 30, 2016

Let me enumerate the big issues with the current syntax:

  1. Confusion about scope and the fact that we're just defining a normal Julia variable (plus registering the symbol in the model and giving a string name)
  2. Confusion about "adding to index sets", e.g., @variable(m, x[1]); @variable(m, x[2]) is a common mistake
  3. Ugly semicolon syntax for conditions on index sets

If we can find a new syntax that addresses all of these issues and is visually appealing then I could be convinced to switch.

CC @ccoffrin @chkwon

@ccoffrin
Copy link
Contributor

ccoffrin commented Oct 3, 2016

I like the idea of lb, ub kwargs in place of <=, >=. I had some confusion about this syntax when I was first learning JuMP.

I am also a fan of using the generator syntax, although it probably does not immediately resolve point 2. I like the original "idea 2" proposals for addressing this issue. When first learning JuMP it was confusing to me that the variable declaration appears to occur inside a function/macro arguments.

In other solver APIs I have seen things along the lines of,

x = [variable(m, INT, lb=0, ub=1) for i in index_set]

Variable constructors only support 1 variable at a time and comprehensions are used for making groups of variables. Inspired by that, here is a proposal,

@variable(<model>, <variable name>, <index set generator>, <type>)
@variable(m, x, [1:10], Int)
@variable(m, y, i in [1:10], Int, lb=0, ub=i)

By disconnecting the index set from the name of the variable, I suspect it would prevent the confusion around being able to add indexes to sets.

If the variable object pointer became the unique key for all variables in a model you could even imagine a syntax along the lines of,

x = variable(m, [1:10], Int)
y = variable(m, i in [1:10], Int, lb=0, ub=i)

@odow
Copy link
Member

odow commented Oct 3, 2016

In response to the points Miles raised:

  1. I like the current syntax. Once you get your head around the scoping, it's clean and makes sense to me. There probably just needs to be some more documentation, especially in the Quick Start Guide highlighting the different scoping cases.
  2. Throw a big warning/error when someone defines a variable with a name that already exists. Maybe have a flag in Model(; uniquevariablenames=true) that can be explicitly turned off (the keyword needs bike-shedding).
  3. Change to generators inside the square bracket of variable index
@variable(m, 0 <= x[i,j for i in X, j in Y if i <= j] <= 1)

I am not in favour of removing the <= and >= options for bounds. One of the things I really like about JuMP is that if you use the plural form of the macros (i.e. @variables), your model file looks virtually identical to the mathematical formulation.

@mlubin
Copy link
Member Author

mlubin commented Oct 3, 2016

Throw a big warning/error when someone defines a variable with a name that already exists.

The reason we didn't do this before was lack of anonymous variables. Now that we have these, I'd be in favor of a warning in this case. I don't even see a compelling reason why we should allow people to disable the warning.

Change to generators inside the square bracket of variable index

The syntax x[i,j for i in X, j in Y if i <= j] doesn't currently parse. And intuitively it doesn't seem obvious that i and j would be in scope to define bounds, for example. I like the direction though.

@mlubin
Copy link
Member Author

mlubin commented Oct 3, 2016

@ccoffrin, if the syntax is @variable(<model>, <variable name>, <index set generator>, <type>) and the variable name is optional, then you have an ambiguity with

@variable(m, S)

where you don't know if you're declaring an anonymous set of variables indexed over S or a single variable named S.

@mlubin
Copy link
Member Author

mlubin commented Oct 4, 2016

Bed time for me by the way. This is very promising and I'll pick this up tomorrow.

@odow
Copy link
Member

odow commented Oct 4, 2016

@variable(m, x::PSDMatrix(N)::Integer in [0, 2])

@chriscoey
Copy link
Contributor

I feel like SemiContinuous(l,u) and Integer in [l,u] look too different. Maybe it should be SemiContinuous in [l,u]. Or Integer(l,u) (but then you need Continuous(l,u) or Interval(l,u) as well really).

@ccoffrin
Copy link
Contributor

ccoffrin commented Oct 4, 2016

I think @odow's case is a case where set intersection reasoning could pay off, something like @variable(m, x in PSDMatrix(N) & Integer & Interval(0, 2)) is clearer to me than nesting ::

@odow
Copy link
Member

odow commented Oct 4, 2016

I think this has a number of things going for it. It plays nicely of Julia's type system. It gets rid of the ternary a <= x <= b issue but people can keep the binary operators if they don't want to use [lower, Inf]. The scoping issue with bounds falls out nicely. The only issue is how to include keyword arguments like start

Single variable type

@variables(m, begin
    x::Continuous
    x in [l, u]    # simple interval
    x <= u         # one sided bound
    x >= l         # one sided bound
    x::Binary
    x::Integer in [l, u]
    x::Integer >= l
    x::SemiContinuous in [l, u]
    x::SemiInteger in [l, u]
    x::PSDMatrix(N)
    x::Symmetric(N)
    x::NonNegative
    x::NonPositive
end)

Nesting variable types or &

@variables(m, begin
    x::PSDMatrix(N) ∩ Integer
    x::Integer & NonPositive
end)

Generators

@variables(m, begin
    x[i,j] >= l for i in I, j in J if i <= j
    x[i,j]::Integer in [l[i], u[j]] for i in I, j in J if i <= j
    x[i,j]::PSDMatrix in [l, u] for i in I, j in J # define your own indices but it must still be square
end)

Anonymous Variables

x = @variable(m, lowerbound=1) # distinguished by the keyword argument
x = @variable(m, ::Continuous >= 1)
x = @variable(m, [i,j]::Integer in [l[i], u[j]] for i in I, j in J if i <= j)

@mlubin
Copy link
Member Author

mlubin commented Oct 4, 2016

A few comments:

x::SemiContinuous in [l, u]

doesn't feel right because x is not restricted to the interval [l,u]. I'd rather have SemiContinuous(l,u).

x::PSDMatrix(N) ∩ Integer

doesn't parse with the right precedence. It should be

x::(PSDMatrix(N) ∩ Integer) # or
x::(PSDMatrix(N) & Integer)

I like how the latter reads as PSD and Integer.

x[i,j]::PSDMatrix in [l, u] for i in I, j in J # define your own indices but it must still be square

This seems confusing because x[i,j] is not restricted to be PSD, the whole matrix is. I'd just disallow generator syntax indexing for matrix variables.

@joehuchette
Copy link
Contributor

joehuchette commented Oct 4, 2016

My thoughts:

  • I'm OK with the x in [0,3] syntax (I prefer square brackets to parens, which I read as open intervals), but I would like to keep >= and friends for one-sided bounds
  • I think using faux type annotations like ::Integer is a little too punny and clever
  • Semicontinuous variables read nicely as {0} | [3,5], and as Miles mentions, this leaves open the option for more generic disjunctions in the future
  • I'm not sure about specifying variable types. I don't find [0,3] & Integer particularly intuitive. What about {0,1} for binary and something like {3,...,10} or 3:10 for general integer (think the second was mentioned earlier)?

@mlubin
Copy link
Member Author

mlubin commented Oct 4, 2016

@joehuchette, I'd agree that x in [0,3] & Integer doesn't read well, but x::Integer in [0,3] certainly does.

@joehuchette
Copy link
Contributor

I disagree. Someone without julia programming experience won't know what the :: operator means. And someone with experience will read it as a type annotation, which it isn't. I still think x in [0,3], Integer is clearer to both groups.

@odow
Copy link
Member

odow commented Oct 4, 2016

The problem I have with variations on {} or allowing disjunctions is that what should JuMP do with: {0, 1, 2}, {0, 2}, {0, 1, 3}, {1} | [2, 3].

If the point of changing the syntax is to make it more intuitive, and JuMP doesn't reformulate these cases, then this goes against that.

I would almost argue that we don't need intersecting types. If there is SemiContinuous and SemiInteger then there should be PSDMatrixInteger and SymmetricInteger. NonNegative and NonPositive become sets:

@variable(m, x::SymmetricInteger(N) in NonNegative)

As a side note/tangent: what are the downsides of stictly typing variables?

abstract JuMP.Variable
immutable Integer <: JuMP.Variable
    m
    idx
end

@joehuchette
Copy link
Contributor

The problem I have with variations on {} or allowing disjunctions is that what should JuMP do with: {0, 1, 2}, {0, 2}, {0, 1, 3}, {1} | [2, 3].

The first is valid (if we choose), while the other three are errors. If we want to support more exotic CP-style set constraints later on, the syntax is already there, and we can just remove the errors.

If the point of changing the syntax is to make it more intuitive, and JuMP doesn't reformulate these cases, then this goes against that.

Why? IMO {0} | [2,3] is so much more intuitive for the lay user than an obscure piece of jargon like SemiContinuous.

As a side note/tangent: what are the downsides of stictly typing variables?

There are some potential compiler optimizations that may be impossible (i.e. packing Variables of different types in-line in vectors), but those are probably negligible. I'm not sure what's to be gained though, besides additional complexity. How would you use dispatch on different variable types?

@mlubin
Copy link
Member Author

mlubin commented Oct 5, 2016

I think we should aim for a well-defined concept of the domain of a variable (or collection of variables) that can be constructed programmatically, and then find the right syntax for it. We're having trouble deciding how integer/bounds/semicontinuous/PSD restrictions interact because it's been pretty ad-hoc up to this point. Now is the right time to look forward to make the concepts and syntax general enough to allow for future extensions to JuMP that could allow unions of domains and/or CP-style domains.

@joehuchette
Copy link
Contributor

joehuchette commented Oct 5, 2016

I agree, but I think the there are two somewhat orthogonal questions here. One is what types/operations to define so that @variable(m, x in D) works, where we can build up D programmatically. The second is whether we want DSL sugar like @variable(m, x in {0} ∪ [3,5]) to make things pretty (for the sets we do currently support, and others in the future). How should this sugar look so that it is most clear, to people writing and reading models? We can add one, both, or neither independently, it seems.

@mlubin
Copy link
Member Author

mlubin commented Oct 5, 2016

@joehuchette, I think it also affects the question of x::Integer, x in Integers or x, Integer.

@joehuchette
Copy link
Contributor

That's a question of syntax, though. Let's choose what we feel is most natural, as you're not going to be programmatically generating that code. If we want to add support for something like

D = Integer(0,5)
@variable(m, x in D)

later, we can.

@mlubin
Copy link
Member Author

mlubin commented Oct 5, 2016

@joehuchette, could you sketch up a proposal analogous to #692 (comment)?

@joehuchette
Copy link
Contributor

joehuchette commented Oct 5, 2016

Single variable type

@variables(m, begin
    x
    x in [l, u]    # simple interval
    x <= u         # one sided bound
    x >= l         # one sided bound
    x in {0,1}
    x in [l, u], Integer
    x >= l, Integer
    x in {0}  [l, u] # SemiContinuous
    x in {0}  [l, u], Integer # SemiInteger
    x[1:N,1:N], PSD
    x[1:N,1:N], Symmetric
    x >= 0 # NonNegative
    x <= 0 # NonPositive
end)

Nesting variable types ∩ or &

@variables(m, begin
    x[1:N,1:N], PSD, Integer
    x <= 0, Integer # NonPositive
end)

Anonymous Variables

x = @variable(m, lowerbound=1) # distinguished by the keyword argument
x = @variable(m, lowerbound=1)

The generators don't play nicely with the variable type specification (or any keyword arguments, for that matter), so that will take some more thought.

@mlubin
Copy link
Member Author

mlubin commented Oct 5, 2016

Well.. we could double down on the :: syntax to address the issue with keyword arguments and generators:

@variable(m, x[i,j]::Integer(name="x[$i,$j]") in [l[i], u[j]] for i in I, j in J if i <= j)
@variable(m, x[i,j]::Integer(name="x[$i,$j]",lowerbound=l[i],upperbound=u[i]) for i in I, j in J if i <= j)

@mlubin
Copy link
Member Author

mlubin commented Oct 5, 2016

Declaring variables shouldn't be that hard, not sure why we would need multiple macros for it.

@yeesian
Copy link
Contributor

yeesian commented Oct 5, 2016

Well.. we could double down on the :: syntax to address the issue with keyword arguments and generators

I think it's quickly becoming confusing* whether the variable[s] macro is dealing with a "domain" or a "variable". Following joey's comment, I can imagine the possibility of something along the lines of

@domain(m, D[i],
    variabletype = Integer,
    lowerbound = l[i],
    upperbound = u[i]
)
@variable(m, x[i,j] in D[i] for i in I, j in J if i <= j)

where

  • the syntax in @domain is up-for-grabs, and can be clever/mathy/etc, but
  • @variable[s] preserves current JuMP behavior (apart from introducing generator syntax, and writing two-sided bounds as simple intervals.)

That allows existing users of JuMP to not worry about a whole new set of rules.

*case in point: name="x[$i,$j]" in #692 (comment)

@yeesian
Copy link
Contributor

yeesian commented Oct 5, 2016

I'm not opposed to new suggestions/syntax. But echoing #692 (comment), it feels alarming when it's supposed to "unify" everything (for existing users to stay in the loop and get their opinions heard), when preferences differ across communities.

Given that it might take a while to assess the suitability of various proposals for describing different domains, providing a separate macro for it allows for existing users to not have to worry too much about keeping up with ideas for syntax innovations.

I'm a fan of the intersecting domain syntax idea. Ultimately I think it would lead to less confusion to only allow one syntax and not the =>, <=. I think it's pretty natural.

Does your preference extend to the constraint[s] macro too?

@mlubin
Copy link
Member Author

mlubin commented Jun 9, 2017

My intent is to close this issue if there's no substantial progress made at the meetup next week. At this point, radical syntax changes should be first implemented and tested by multiple people in a separate package before we'd force them onto JuMP users.

@mlubin mlubin added this to the 1.0 milestone Jun 9, 2017
@joehuchette
Copy link
Contributor

I think set inclusion syntax for variables would go a long way, and would be relatively easy to add.

@mlubin
Copy link
Member Author

mlubin commented Jun 10, 2017 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests