Can I build an expression on dual variables and then query with JuMP.dual?

Consider this simple example

import JuMP, Gurobi
model = JuMP.Model(Gurobi.Optimizer)
JuMP.@variable(model, x[1:3])
JuMP.@constraint(model, c1, x .<= 2)
JuMP.@constraint(model, c2, x .>= -2)
JuMP.@objective(model, Min, 0.4 * sum(x))
JuMP.optimize!(model); JuMP.assert_is_solved_and_feasible(model)
JuMP.value.(x)
JuMP.dual.(c1)

We know that JuMP can manipulate primal variable/expressions flexibly, via @expression.
e.g. before optimize!, I can build @expression(model, expr, -x[3] - x[2]), then after optimize!, I can query with JuMP.value(expr). Note that building expr is one-off, if we don’t have this functionality, we have to write the cumbersome -value(x[3]) - value(x[2]) every time after an optimize!.

Now the question is

  • can I also enjoy the similar convenience on dual variables?

One thing that I don’t fathom is, as long as I can call JuMP.dual(c1[1]), c1[1] should be deemed a dual variable? But it turns out to be a JuMP.ConstraintRef. This implies that we can also call JuMP.value(c1[1]) properly after optimize!, which is somewhat unusual. (Are there really people intend to do this, is it often, I don’t quite understand the point of this query)

The current behavior is that, we can only get raw material from JuMP.dual after optimize!, e.g. I can do

optimize!(model)
c1v = -1 * JuMP.dual.(c1) # to make the resulting value positive
c2v = JuMP.dual.(c2)
cv = [c1v; c2v] # store them in a compact array to facilitate carrying

The hope is that Can we do this before optimize!? Something like

JuMP.@dual_expression(model, cv, [-c1; c2]) # setting this is one-off
optimize!(model) # we may do this multiple times
JuMP.dual.(cv) # this style is concise and desirable

In JuMP v1.25, we cannot yet.

can I also enjoy the similar convenience on dual variables

No. JuMP does not have an explicit representation of dual variables.

c1[1] should be deemed a dual variable

No. c1 is a reference to the constraint. A constraint has both a primal value (the value of f(x) from f(x) in set) and a dual value.

The hope is that Can we do this before optimize!? Something like JuMP.@dual_expression(model, cv, [-c1; c2])

This is not possible. And I don’t particularly want to add this.

Among the many reasons, JuMP’s primal variables are scalars, and we use Julia collections to construct Vector, Matrix of them, etc. Constraints (and their primal/dual values) have a shape. So constructing expression of them is non-trivial.

Sounds unusual. I thought it would be sufficient to have value of expressions after optimize!.
Since constraints are built on top of expressions.

Does this mean that it is superfluous to write @expression(model, c, [c1; c2]), since we can write c = [c1; c2] directly?

Although JuMP has concise grammar, e.g. with the help of macros, it is not a bad idea to use Julia’s primitive grammar? e.g. adding that sparse constraint yesterday where we employ 4 for-loops.

Since you’ve been asking a number of “why does JuMP do this” type questions recently, you might find it interesting to do some background reading on why we chose the current design.

On the f(x) in set design, read: [2002.03447] MathOptInterface: a data structure for mathematical optimization problems

For the macro design, watch this part of Miles’ 2017 JuMP-dev talk: https://youtu.be/JaA302TfI7I?feature=shared&t=366 and read Performance tips · JuMP

There’s also this: [2206.03866] JuMP 1.0: Recent improvements to a modeling language for mathematical optimization

You can also use @macroexpand to see the code that JuMP is actually running:

julia> using JuMP

julia> begin
           model = Model()
           @variable(model, x[1:3])
           @constraint(model, c1, x .<= 2)
           @constraint(model, c2, x .>= -2)
           @macroexpand @expression(model, c, [c1; c2])
       end
quote
    #= REPL[52]:6 =#
    JuMP._valid_model(model, :model)
    begin
        #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros.jl:407 =#
        JuMP._error_if_cannot_register(model, :c)
        #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros.jl:408 =#
        c = (model[:c] = begin
                    #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros.jl:399 =#
                    let model = model
                        #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros.jl:400 =#
                        begin
                            #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros/@expression.jl:86 =#
                            begin
                                #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros.jl:264 =#
                                var"#570###397" = begin
                                        #= /Users/oscar/.julia/packages/MutableArithmetics/tNSBd/src/rewrite.jl:374 =#
                                        let
                                            #= /Users/oscar/.julia/packages/MutableArithmetics/tNSBd/src/rewrite.jl:375 =#
                                            begin
                                                #= /Users/oscar/.julia/packages/MutableArithmetics/tNSBd/src/rewrite.jl:371 =#
                                            end
                                            #= /Users/oscar/.julia/packages/MutableArithmetics/tNSBd/src/rewrite.jl:376 =#
                                            [c1; c2]
                                        end
                                    end
                                #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros.jl:265 =#
                                var"#571###398" = (JuMP.flatten!)((MutableArithmetics).copy_if_mutable(var"#570###397"))
                            end
                            #= /Users/oscar/.julia/packages/JuMP/RGIK3/src/macros/@expression.jl:89 =#
                            JuMP._replace_zero(model, var"#571###398")
                        end
                    end
                end)
    end
end

Strip away the fluff and you’ll find

JuMP._valid_model(model, :model)
JuMP._error_if_cannot_register(model, :c)
c = model[:c] = let model = model
    var"#570###397" = [c1; c2]
    var"#571###398" = (JuMP.flatten!)((MutableArithmetics).copy_if_mutable(var"#570###397"))
    JuMP._replace_zero(model, var"#571###398")
end

So it is exactly c = [c1; c2], plus some extra stuff.

1 Like