Following JuliaCon, I wanted to try out the nonlinear version of JuMP, especially the option of having vectors instead of a list of expressions. In the process I managed to
- Make the nonlinear version work without the
@NL
macros, but still with a list of expressions, - Get three different errors: a classic
no method matching +(::VariableRef, ::Vector{AbstractJuMPScalar})
, an Ipopt errorEXIT: Problem has too few degrees of freedom.
, andno method matching zero(::Type{Vector{AbstractJuMPScalar}})
if I tried to make a vector - Get a different number of iterations for the same problem using @NL macros and the new version, and imposing constraints directly in
@variable
or by@constraint
.
Here we go.
Technical: I used od/nlp-expr
`(not sure if correct because I cannot remember which one was mentioned during JuliaCon )
Optimization problem (artificial):
\min z
s.t.
(x_1(0)-x_{10})^2+(x_2(0)-x_{20})^2 \leq z, \forall \mu
\dot{x}_1(t)= x_2(t)
\dot{x}_2(t)=\mu(1-x^2_1(t))x_2(t)-x_1(t)
where \mu=1,2, t=0,0.01,\ldots,20, x_{10}=2, x_{20}=0. Since we have two values of \mu, let’s say \mu_1, \mu_2, we end up with an extended formulation:
\min z
s.t.
(x_{1,\mu_1}(0)-x_{10})^2+(x_{2,\mu_1}(0)-x_{20})^2 \leq z
(x_{1,\mu_2}(0)-x_{10})^2+(x_{2,\mu_2}(0)-x_{20})^2 \leq z
\dot{x}_{1,\mu_1}(t)= x_{2,\mu_1}(t)
\dot{x}_{2,\mu_1}(t)=\mu_1(1-x_{1,\mu_1}^2(t))x_{2,\mu_1}(t)-x_{1,\mu_1}(t)
\dot{x}_{1,\mu_2}(t)= x_{2,\mu_2}(t)
\dot{x}_{2,\mu_2}(t)=\mu_2(1-x^2_{1,\mu_2}(t))x_{2,\mu_2}(t)-x_{1,\mu_2}(t)
The main idea is put the dynamics into a three-dimensional array indexed by time (index j=1:2001
), version of \mu (index k=1:2
), and number of equation (index m=1:2
). It is indexing by the number of equation that fails.
First a version that works, without @NL
macros:
Summary
using JuMP, Ipopt, Plots
tf = 20 #Final time, exact value doesn't matter
Δt = 0.01
n = Int64(round(tf / Δt + 1))
scenariosMu = [1.0 2.0] #####Values don't matter
noScenarios = length(scenariosMu)
vdP = Model(Ipopt.Optimizer)
x_0 = [2.0, 0.0]
@variables(vdP,
begin
x[j=1:n, m=1:2, k=1:noScenarios]
end
)
@expressions(
vdP,
begin
#initial conditions
init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
#RHS of ODE - ideally I would like to avoid having every equation separately, but this works
x1_ODE[j=1:n-1, k=1:noScenarios], x[j,2,k]
x2_ODE[j=1:n-1, k=1:noScenarios], scenariosMu[k]*(1.0-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k]
end
)
@constraint(vdP,[j=1:n-1,k=1:noScenarios], x[j+1, 1, k] == x[j, 1, k] + Δt * x1_ODE[j, k])
@constraint(vdP,[j=1:n-1,k=1:noScenarios], x[j+1, 2, k] == x[j, 2, k] + Δt * x2_ODE[j, k])
@expression(vdP, objMin[k=1:noScenarios], sum(init_x[m,k]*init_x[m,k] for m=1:2))
@variable(vdP, zVar)
@constraint(vdP,[k=1:noScenarios],objMin[k]<=zVar)
@objective(vdP, Min, zVar)
optimize!(vdP)
plot(value.(x)[:,1,2],value.(x)[:,2,2])
plot!(value.(x)[:,1,2],value.(x)[:,2,2])
The three different errors (I marked in each formulation what is different):
Summary
using JuMP, Ipopt, Plots
tf = 20 #Final time, exact value doesn't matter
Δt = 0.01
n = Int64(round(tf / Δt + 1))
scenariosMu = [1.0 2.0] #####Values don't matter
noScenarios = length(scenariosMu)
vdP = Model(Ipopt.Optimizer)
x_0 = [2.0, 0.0]
@variables(vdP,
begin
x[j=1:n, m=1:2, k=1:noScenarios]
end
)
# ######################################Doesn't work: no method matching +(::VariableRef, ::Vector{AbstractJuMPScalar})
# @expressions(
# vdP,
# begin
# #initial conditions
# init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
# #RHS of ODE
##########CHANGE BEGINS HERE - one big vector of RHS
# x_ODE[j=1:n-1,m=1:2, k=1:noScenarios], [
# x[j,2,k]
# scenariosMu[k]*(1.0-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k]
# ]
##########CHANGE ENDS HERE
# end
# )
##########CHANGE BEGINS HERE - everything is indexed together by j, m, k
# @constraint(vdP,[j=1:n-1,m=1:2,k=1:noScenarios], x[j+1, m, k] == x[j, m, k] + Δt * x_ODE[j,m, k])
##########CHANGE ENDS HERE
# ######################################
######################################Doesn't work: Ipopt error:
# Exception of type: TOO_FEW_DOF in file "Interfaces/IpIpoptApplication.cpp" at line 655:
# Exception message: status != TOO_FEW_DEGREES_OF_FREEDOM evaluated false: Too few degrees of freedom (rethrown)!
# EXIT: Problem has too few degrees of freedom.
# @expressions(
# vdP,
# begin
# #initial conditions
# init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
# #RHS of ODE
# x_ODE[j=1:n-1,m=1:2, k=1:noScenarios], [
# x[j,2,k]
# scenariosMu[k]*(1.0-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k]
# ]
# end
# )
##########CHANGE BEGINS HERE - broadcasting because the error said + couldn't work
# @constraint(vdP,[j=1:n-1,m=1:2,k=1:noScenarios], x[j+1, m, k] .== x[j, m, k] .+ Δt * x_ODE[j,m, k])
##########CHANGE ENDS HERE
######################################
# ######################################Doesn't work: no method matching zero(::Type{Vector{AbstractJuMPScalar}})
# @expressions(
# vdP,
# begin
# #initial conditions
# init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
# #RHS of ODE
# x_ODE[j=1:n-1,m=1:2, k=1:noScenarios], [
# x[j,2,k]
# scenariosMu[k]*(1.0-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k]
# ]
# end
# )
##########CHANGE BEGINS HERE - no broadcasting, but I wanted to reproduce the error from a more complex example so I added constant terms to RHS: 1-1
# @constraint(vdP,[j=1:n-1,m=1:2,k=1:noScenarios], x[j+1, m, k] == 1.0-1.0+x[j, m, k] + Δt * x_ODE[j,m, k])
##########CHANGE ENDS HERE
# ######################################
@expression(vdP, objMin[k=1:noScenarios], sum(init_x[m,k]*init_x[m,k] for m=1:2))
@variable(vdP, zVar >= 0)
@constraint(vdP,[k=1:noScenarios],objMin[k]<=zVar)
@objective(vdP, Min, zVar)
optimize!(vdP)
And finally, the same problem implemented with @NL macros:
Summary
######################################WORKS
tf = 20 #Final time, exact value doesn't matter
Δt = 0.01
n = Int64(round(tf / Δt + 1))
scenariosMu = [1.0 2.0] #####Values don't matter
noScenarios = length(scenariosMu)
vdP = Model(Ipopt.Optimizer)
x_0 = [2.0, 0.0]
@variables(vdP,
begin
x[j=1:n, m=1:2, k=1:noScenarios]
end
)
@NLexpressions(
vdP,
begin
#initial conditions
init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
#RHS of ODE
x1_ODE[j=1:n-1, k=1:noScenarios], x[j,2,k]
x2_ODE[j=1:n-1, k=1:noScenarios], scenariosMu[k]*(1.0-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k]
end
)
@NLconstraint(vdP,[j=1:n-1,k=1:noScenarios], x[j+1, 1, k] == x[j, 1, k] + Δt * x1_ODE[j, k])
@NLconstraint(vdP,[j=1:n-1,k=1:noScenarios], x[j+1, 2, k] == x[j, 2, k] + Δt * x2_ODE[j, k])
@NLexpression(vdP, objMin[k=1:noScenarios], sum(init_x[m,k]*init_x[m,k] for m=1:2))
@variable(vdP, zVar)
@NLconstraint(vdP,[k=1:noScenarios],objMin[k]<=zVar)
@NLobjective(vdP, Min, zVar)
optimize!(vdP)
plot!(value.(x)[:,1,2],value.(x)[:,2,2],ls=:dash)
plot!(value.(x)[:,1,2],value.(x)[:,2,2],ls=:dash)
If I do not have any constraints on z
, then the new version needs 146 iterations whereas @NL
needs 252 iterations. The difference in iterations changes in the other direction if I impose an additional constraint on z
: @constraint(vdP,z>=0)
. Then the new version needs 603 iterations whereas @NL
version needs 461 iterations. If I impose the constraint on z
directly when creating the variable @variable(vdP,zVar>=0)
, then the new version needs 1266 iterations whereas @NL
version needs 442 iterations. I get that the number of iterations may change with the number of constraints, so the differences between no constraints and constraints make sense. I am not sure why I get a difference for two different formulations of the same constraint.
As a last thing, I noticed that this works with/without broadcast . after 1.0:
@expressions(
vdP,
begin
#initial conditions
init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
#RHS of ODE
x1_ODE[j=1:n-1, k=1:noScenarios], x[j,2,k]
x2_ODE[j=1:n-1, k=1:noScenarios], scenariosMu[k]*(1.0.-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k] ####<-
end
)
but this works only without broadcast . after 1.0:
@NLexpressions(
vdP,
begin
#initial conditions
init_x[m=1:2, k=1:noScenarios], x[1, m, k] - x_0[m]
#RHS of ODE
x1_ODE[j=1:n-1, k=1:noScenarios], x[j,2,k]
x2_ODE[j=1:n-1, k=1:noScenarios], scenariosMu[k]*(1.0-x[j,1,k]*x[j,1,k])*x[j,2,k]-x[j,1,k] ####<-
end
)
With .
, JuMP throws an error Unrecognized function ".-" used in nonlinear expression.
I guess this is related to the rule of “no vectors in nonlinear expressions”, right?
Questions:
- Is it possible to avoid writing out the equations one by one?
- What’s different in the
@NL
formulation that makes the number of iterations different? - Why having a number in the constraint changes the error if there is already a number in the expression?