Is there a reason to use `@expression`? I think it's redundant

Sometimes I build incorrect expressions obliviously, but JuMP won’t throw an ERROR. Is this good?

import JuMP, Gurobi
model1 = JuMP.Model(Gurobi.Optimizer);
JuMP.@variable(model1, x1 >= 1);
JuMP.@objective(model1, Min, x1);
JuMP.optimize!(model1);

model2 = JuMP.Model(Gurobi.Optimizer);
JuMP.@variable(model2, x2);
JuMP.@objective(model2, Min, x2);

# I miswrote `model2` as `model1` here
expr_miswrote = JuMP.@expression(model1, JuMP.value(x1)x2)
# But I fail to receive a proper ERROR
# And it can work normally later on!
JuMP.@constraint(model2, expr_miswrote >= 2)
JuMP.optimize!(model2)
JuMP.assert_is_solved_and_feasible(model2; allow_local = false)
@assert JuMP.objective_value(model2) == 2

Two unusual things to note:

  1. JuMP.value(x1)x2 belongs to model2 indeed, but it can be associated to model1.
  2. At least by inspecting the code, expr_miswrote belongs to model1, but it can be inserted into a model2’s constraint.

This is expected behavior, even if it might look confusing.

Your expr_miswrote contains variables from model2. But you can “add” it as an expression to model1 because it never actually gets added to the objective or constraints of model1.

We allow it because you can actually add expressions of any type to a model.

julia> using JuMP

julia> model = Model();

julia> @expression(model, my_expr[i in 1:2], Dict(:foo => i))
2-element Vector{Dict{Symbol, Int64}}:
 Dict(:foo => 1)
 Dict(:foo => 2)

julia> model[:my_expr]
2-element Vector{Dict{Symbol, Int64}}:
 Dict(:foo => 1)
 Dict(:foo => 2)

You can use expr_miswrote in a constraint of model2 because that is perfectly legal: it contains only variables from model2.

This makes some sense as my_expr becomes one of the belongings of model.

But the arg model1 in this anonymous usage becomes pointless.
To build an anonymous expression, it suffices to write
expr = JuMP.@build_expression(JuMP.value(x1)x2)
resembling
con = JuMP.@build_constraint(expr <= 2), no?

To build an anonymous expression, it suffices to write

I assume you meant this as a potential feature request. Currently @build_expression doesn’t exist.

I don’t plan to make any changes to JuMP related to this issue. Yes, you can create expression in the “wrong” model, but changing this is a breaking change we won’t be doing. We also won’t be adding any new macros just to support anonymous expressions that are not affiliated with a model.

No, I just express my thoughts.

Maybe this can be improved in JuMP 2.0 or some version that permit breaking changes.

We have no plans to work on or release JuMP 2.0

1 Like

The construct @expression is odd—I surmise that in all circumstances it’s better to bypass using it, so we can remove the @expression API entirely.

Say,

  • I want to create a new expression e using existing variables x and y.

The standard way at present:

@expression(model, e, 1x + 2y)

Instead of it, Why not write:

@variable(model, e == 1x + 2y) # I currently propose this grammar

so that

JuMP.is_fixed(e) # true

JuMP.FixRef(e) # refering to the constraint e == 1x + 2y built above

JuMP.is_integer(e) # false

JuMP.set_integer(e) # you can do this

Additionally you can do e.g.

JuMP.set_upper_bound(e, 9)

If you also want to “fix e to 4” in the old sense, despite being highly unusual and unlikely in practice, you can still do

JuMP.@constraint(model, c, e == 4)

so we won’t be losing anything.


If a user want to write

@variable(model, x == 4)

in the old sense.

I would say: if he doesn’t need dual variable, then he can write
set_upper_bound + set_lower_bound at the same time to bypass.

If he need the dual associated to x == 4, then probably x will not serve as an expression.
so the new grammar proposed by me won’t clash with his intention.

AMPL calls these “defined variables”: https://ampl.com/wp-content/uploads/Chapter-8-Linear-Programs-Variables-Objectives-and-Constraints-AMPL-Book.pdf

We could have chosen to do something similar, but we didn’t.

so we can remove the @expression API entirely.

Since JuMP has reached v1, we will never do this.

As a corollary, since we have @expression, we won’t be adding a way to replicate it via @variable.

1 Like

Is this a bug?

julia> import JuMP

julia> model = JuMP.Model();

julia> x = map(_ -> JuMP.@variable(model), 1) # returns a SCALAR
_[1]

julia> ndims(x), ndims(1)
(0, 0)

julia> map(_ -> JuMP.@variable(model), x) # returns a Vector???
1-element Vector{JuMP.VariableRef}:
 _[2]

I think the last return should be _[2] solely, not wrapped in a Vector.
Yes, surely I think _[2] should be the expected return, otherwise duplicating the following:

julia> model = JuMP.Model();

julia> x = map(_ -> JuMP.@variable(model), 1)
_[1]

julia> map(_ -> JuMP.@variable(model), [x])
1-element Vector{JuMP.VariableRef}:
 _[2]

The julia’s behavior is sane:

julia> map(_ -> 1, 2)
1

julia> map(_ -> 1, [2])
1-element Vector{Int64}:
 1

This behaviour isn’t specific to JuMP:

julia> struct Foo end

julia> Base.length(::Foo) = 1

julia> Base.iterate(f::Foo) = (f, true)

julia> Base.iterate(f::Foo, state) = nothing

julia> map(_ -> :a, 1)
:a

julia> map(_ -> :a, Foo())
1-element Vector{Symbol}:
 :a

There’s special fallback for Number:

Arguably that is the inconsistent one here.

We probably won’t be changing the current behaviour in JuMP. It’s also arguably correct, so a change is breaking. But also. Why would you map over a variable. Just don’t do that.

If I had a comment, it’s that many of your recent Discourse questions are getting at edge cases of Julia. Sure, some of them are confusing. But in every case, it’s because you are writing code that is not idiomatic.

1 Like

Because my API works inconsistently. I don’t think there is anywhere wrong with my macro API.

julia> macro get_int_decision(model, expr) return esc(quote
           let e = JuMP.@expression($model, $expr)
               local a = map(_ -> JuMP.@variable($model, integer = true), e)        
               JuMP.@constraint($model, a == e)
               a
           end
       end) end
@get_int_decision (macro with 1 method)

julia> 

julia> import JuMP

julia> m = JuMP.Model();

julia> JuMP.@variable(m, x);

julia> @get_int_decision(m, [1x, 2x]); # okay

julia> @get_int_decision(m, 1x) # ERROR
ERROR: Subtraction between an array and a JuMP scalar is not supported: instead of `x - y`, do `x .- y` for element-wise subtraction.

I think there should be a counterpart in JuMP. Since

julia> ndims(x), ndims(1)
(0, 0)

, they should be consistent. Otherwise driving anyone who has the slightest bit of knowledge about linear algebra mad.

This should be breaked. The existing behavior is wrong. (And it’s inconsistent with the Julia’s spirit on Linear Algebra). x::VariableRef is the JuMP’s counterpart of 1.0::Float64 in julia—linked by JuMP.value

You probably want JuMP.@constraint($model, a .== e).

There are many inconsistencies in Julia. It doesn’t have strong interfaces so there is no reason one is more correct than another.

You probably want

No this is not decent. If the RHS is a scalar the LHS is a Array I would use .==.
But here I’m certain that they should be the same shape, therefore I would use the special method == exclusive for vector relation (doing things in a Vector Space in the math sense).

It doesn’t have strong interfaces

What does this mean?


Edit: Okay, I know it’s a bit vague, let’s shelve this issue.:upside_down_face:

Currently there are 2 ways out:

  1. use broadcast instead of map
  2. add a ndims branch before using map

I think I prefer the latter.

julia> import JuMP.value as ı

julia> x # a VariableRef after optimize!
_[1]

julia> ı(x) # get its value as a scalar
0.0

julia> f = z -> round(Bool, ı(z));

julia> f(x) # to be saved as a Bool
false

julia> f.([x, 1x]) # works
2-element BitVector:
 0
 0

julia> f.(x) # This is correct though
false

julia> broadcast(f, x) # unlike `map`, this returns an expected object
false

julia> function get_Bool_value(x) # I prefer this API
           f = z -> round(Bool, ı(z))
           ndims(x) == 0 && return f(x)
           map(f, x)
       end

julia> get_Bool_value(x) # works for scalar
false

julia> get_Bool_value([x, 1x]) # works for arrays
2-element Vector{Bool}:
 0
 0

I somewhat doubt the performance of broadcasting—another reason for me to prefer map. e.g. Add vector version for setting constraint attributes by blegat · Pull Request #4033 · jump-dev/JuMP.jl · GitHub