Dual variables of the lower level
This is a quick introduction to modeling and solving bilevel optimization with BilevelJuMP.
This modeling feature enables the implementation of workflows where one (or more) of the upper-level variables is the dual of a lower-level constraint. In particular, in the energy sector, it is common to model the energy prices as the dual variable associated with the energy demand equilibrium constraint. One example of an application that uses this feature is Fanzeres et al. (2019), which focuses on strategic bidding in auction-based energy markets. A small and simplified example of the modeled problem would be the model:
\[\begin{align} &\max_{\lambda, q_S} \quad \lambda \cdot g_S \\ &\textit{s.t.} \quad 0 \leq q_S \leq 100\\ &\hspace{28pt} (g_S, \lambda) \in \arg\min_{g_S, g_{1}, g_{2}, g_D} 50 g_{R1} + 100 g_{R2} + 1000 g_{D}\\ & \hspace{70pt} \textit{s.t.} \quad g_S \leq q_S \\ & \hspace{88pt} \quad 0 \leq g_S \leq 100 \\ & \hspace{88pt}\quad 0 \leq g_{1} \leq 40 \\ & \hspace{88pt}\quad 0 \leq g_{2} \leq 40 \\ & \hspace{88pt}\quad 0 \leq g_{D} \leq 100 \\ & \hspace{88pt}\quad g_S + g_{1} + g_{2} + g_D = 100 \quad : \quad \lambda \label{eq-dual-lambda} \end{align}\]
Where $\lambda$ is the dual of the load balance constraint (last constraint in the lower part), $g_S$, $g_{1}$, $g_2$ represent the generation of the strategic bidder and from two other (non-strategic) plants. $g_D$ represents the deficit in generation. Finally, $q_S$ is the quantity bid optimized by the strategic generator.
load packages
using BilevelJuMP
using Ipopt
using QuadraticToBinary
using HiGHSBilevelJuMP.jl allows users to implement similar models using the function DualOf that binds a new variable in the upper level to an existing constraint in the lower level. The model can be written as:
model = BilevelModel()
@variable(Upper(model), 0 <= qS <= 100)
@variable(Lower(model), 0 <= gS <= 100)
@variable(Lower(model), 0 <= gR1 <= 40)
@variable(Lower(model), 0 <= gR2 <= 40)
@variable(Lower(model), 0 <= gD <= 100)
@objective(Lower(model), Min, 50gR1 + 100gR2 + 1000gD)
@constraint(Lower(model), gS <= qS)
@constraint(Lower(model), demand_equilibrium, gS + gR1 + gR2 + gD == 100)
@variable(Upper(model), lambda, DualOf(demand_equilibrium))
@objective(Upper(model), Max, lambda*gS)$ lambda\times gS $
NLP solution
This model, can be solved by selecting a reformulation and a solver. Here we select Strong-Duality reformulation, the Ipopt solver and call optimizes to perform the reformulation and solve it.
BilevelJuMP.set_mode(model, BilevelJuMP.StrongDualityMode())
set_optimizer(model, Ipopt.Optimizer)
optimize!(model)This is Ipopt version 3.14.4, running with linear solver MUMPS 5.4.1.
Number of nonzeros in equality constraint Jacobian...: 17
Number of nonzeros in inequality constraint Jacobian.: 22
Number of nonzeros in Lagrangian Hessian.............: 3
Total number of variables............................: 15
variables with only lower bounds: 4
variables with lower and upper bounds: 5
variables with only upper bounds: 5
Total number of equality constraints.................: 5
Total number of inequality constraints...............: 3
inequality constraints with only lower bounds: 1
inequality constraints with lower and upper bounds: 0
inequality constraints with only upper bounds: 2
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
0 0.0000000e+00 1.00e+03 1.28e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
1 9.4967374e-05 1.00e+03 1.27e+00 -1.0 3.44e+02 - 2.88e-05 2.90e-05h 1
2 8.1838617e-04 1.00e+03 1.67e+00 -1.0 9.37e+02 - 2.13e-05 4.45e-04f 1
3 2.6260004e-03 9.99e+02 1.64e+00 -1.0 9.16e+02 - 5.75e-04 5.48e-04h 1
4 4.2939366e-01 9.72e+02 3.70e+00 -1.0 9.08e+02 - 7.13e-04 2.68e-02f 1
5 8.9922740e-01 9.69e+02 3.69e+00 -1.0 7.99e+02 - 4.77e-03 3.50e-03f 1
6 1.5988102e+00 9.68e+02 3.69e+00 -1.0 8.64e+02 - 4.71e-04 3.58e-04f 1
7 8.1726295e+00 9.67e+02 3.65e+00 -1.0 1.79e+03 - 3.47e-04 1.12e-03f 1
8 9.1199253e+00 9.67e+02 3.70e+00 -1.0 9.95e+02 - 8.44e-04 2.73e-04f 1
9 1.1465721e+01 9.66e+02 3.60e+00 -1.0 2.01e+03 - 8.77e-05 6.96e-04f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
10 1.4960863e+01 9.64e+02 3.55e+00 -1.0 6.96e+02 0.0 1.22e-03 2.64e-03f 1
11 1.6509553e+01 9.63e+02 5.48e+00 -1.0 6.42e+02 -0.5 9.07e-03 9.47e-04f 1
12 2.0601057e+01 9.61e+02 5.71e+00 -1.0 5.08e+02 -1.0 2.37e-03 1.83e-03f 1
13 2.8419542e+01 9.61e+02 5.09e+00 -1.0 1.48e+06 - 9.26e-07 1.35e-06f 1
14 4.1802970e+01 9.61e+02 5.47e+00 -1.0 4.68e+06 - 8.13e-07 7.30e-07f 1
15 7.4957309e+01 9.61e+02 4.56e+00 -1.0 2.03e+07 - 3.72e-07 4.18e-07f 1
16 1.3881250e+02 9.61e+02 5.16e+00 -1.0 9.66e+06 - 1.75e-06 1.69e-06f 1
17 1.5193761e+02 9.61e+02 5.21e+01 -1.0 1.00e+06 - 5.04e-05 3.34e-06f 1
18 1.5247861e+02 9.61e+02 1.20e+02 -1.0 1.57e+03 - 4.47e-02 5.47e-05f 1
19 8.6224578e+04 6.37e+04 7.78e+02 -1.0 9.64e+02 - 4.68e-04 9.24e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
20 8.6928452e+04 6.37e+04 7.83e+02 -1.0 1.39e+06 - 4.65e-05 1.36e-04f 1
21 8.6931703e+04 6.37e+04 7.49e+02 -1.0 1.16e+04 - 7.85e-02 1.58e-04f 1
22 8.6310221e+04 6.22e+04 7.75e+02 -1.0 4.34e+02 - 3.75e-01 2.32e-02h 1
23 8.6303985e+04 6.22e+04 9.11e+02 -1.0 4.25e+02 - 3.24e-01 2.37e-04h 1
24 8.6265395e+04 6.21e+04 7.98e+02 -1.0 7.31e+01 - 8.46e-06 7.03e-04h 1
25 6.8704198e+04 4.52e+04 5.79e+02 -1.0 8.35e+01 - 6.37e-04 2.67e-01h 1
26 5.4467029e+04 3.14e+04 4.06e+02 -1.0 5.85e+01 - 3.38e-01 2.99e-01h 1
27 5.4323041e+04 3.13e+04 4.45e+02 -1.0 3.94e+01 - 2.91e-01 4.44e-03h 1
28 5.4102419e+04 3.11e+04 4.01e+02 -1.0 4.12e+01 - 5.49e-05 6.40e-03h 1
29 3.3729198e+04 1.25e+04 2.41e+02 -1.0 4.12e+01 - 2.15e-02 5.81e-01h 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
30 2.5938021e+04 5.43e+03 8.48e+01 -1.0 1.60e+01 - 9.96e-01 5.61e-01h 1
31 2.0420516e+04 3.86e+02 6.64e+00 -1.0 6.82e+00 - 1.00e+00 9.22e-01h 1
32 2.0336070e+04 3.09e+02 2.51e+02 -1.0 5.14e-01 - 1.00e+00 2.00e-01h 1
33 2.0109529e+04 1.01e+02 1.91e+02 -1.0 4.11e-01 - 1.00e+00 6.72e-01h 1
34 2.0046678e+04 4.36e+01 5.31e+02 -1.0 1.35e-01 - 1.00e+00 5.69e-01h 1
35 2.0018605e+04 1.79e+01 1.20e+03 -1.0 5.80e-02 - 1.00e+00 5.90e-01h 1
36 2.0007221e+04 7.43e+00 2.93e+03 -1.0 2.38e-02 - 1.00e+00 5.84e-01h 1
37 2.0002490e+04 3.07e+00 7.02e+03 -1.0 9.86e-03 - 1.00e+00 5.86e-01h 1
38 2.0000590e+04 1.27e+00 1.70e+04 -1.0 4.00e-03 - 1.00e+00 5.86e-01h 1
39 1.9999939e+04 5.66e-01 4.39e+04 -1.0 1.37e-03 - 1.00e+00 5.56e-01h 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
40 1.9999916e+04 5.10e-01 2.05e+05 -1.0 2.97e-03 - 1.00e+00 9.97e-02f 3
41 1.9999819e+04 1.55e-01 1.25e+05 -1.0 3.12e-03 - 1.00e+00 6.96e-01h 1
42 1.9999819e+04 1.43e-01 7.31e+05 -1.0 1.22e-03 - 1.00e+00 7.51e-02f 4
43 1.9999806e+04 2.90e-02 2.92e+05 -1.0 1.08e-03 - 1.00e+00 7.98e-01h 1
44 1.9999805e+04 2.55e-02 3.26e+06 -1.0 1.66e-04 - 1.00e+00 1.21e-01f 3
45 1.9999800e+04 6.64e-03 1.82e+06 -1.0 1.64e-04 - 1.00e+00 7.39e-01h 1
46 1.9999800e+04 6.11e-03 1.31e+07 -1.0 5.25e-05 - 1.00e+00 8.04e-02f 4
47 1.9999800e+04 9.57e-04 3.47e+06 -1.0 4.59e-05 - 1.00e+00 8.59e-01h 1
48 1.9999800e+04 6.28e-04 2.79e+07 -1.0 9.71e-06 - 1.00e+00 3.06e-01f 2
49 1.9999799e+04 8.69e-05 3.54e+07 -1.0 1.15e-05 - 1.00e+00 2.50e-01h 3
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
50 1.9999800e+04 4.22e-06 4.18e+07 -1.0 1.12e-05 - 1.00e+00 3.12e-02h 6
51 1.9999800e+04 5.78e-07 4.41e+07 -1.0 1.30e-05 - 1.00e+00 1.95e-03h 10
52 1.9999800e+04 5.78e-07 4.47e+07 -1.0 1.29e-05 - 1.00e+00 1.22e-04h 14
53 1.9999808e+04 7.72e-03 8.23e-03 -1.0 1.40e-05 - 1.00e+00 1.00e+00s 22
54 1.9999997e+04 4.41e-07 4.92e+00 -5.7 4.93e-03 - 1.00e+00 1.00e+00f 1
55 1.9999999e+04 1.48e-10 1.25e+03 -5.7 4.82e-05 - 9.51e-01 9.97e-01f 1
56 2.0000000e+04 7.82e-13 6.55e+00 -5.7 4.65e-06 - 1.00e+00 9.95e-01f 1
57 2.0000000e+04 9.99e-14 4.70e-10 -5.7 5.51e-08 - 1.00e+00 1.00e+00f 1
58 2.0000000e+04 1.14e-13 7.81e+01 -8.6 9.21e-08 - 9.58e-01 1.00e+00f 1
59 6.1246727e+04 3.72e+04 2.78e+13 -8.6 1.21e+78 - 5.73e-30 2.12e-18F 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
60 6.1246728e+04 3.72e+04 2.78e+13 -8.6 4.52e+04 - 8.31e-03 2.19e-10h 1
61 6.1144734e+04 3.71e+04 2.55e+13 -8.6 3.92e+03 - 5.75e-02 8.18e-02h 1
62r 6.1144734e+04 3.71e+04 1.00e+03 3.6 0.00e+00 - 0.00e+00 4.41e-11R 2
63r 6.2346762e+04 3.83e+04 2.03e+03 3.6 2.30e+12 - 9.22e-10 4.86e-11f 6
64r 6.0677876e+04 3.62e+04 2.03e+03 1.5 2.55e+06 - 6.82e-04 6.06e-04f 1
65r 5.9593573e+04 3.42e+04 2.03e+03 1.5 8.11e+05 - 6.73e-06 1.29e-03f 1
66r 3.3177905e+04 9.07e+03 2.03e+03 1.5 1.34e+06 - 1.42e-05 1.87e-03f 1
67 3.3166193e+04 9.06e+03 9.99e+02 -8.6 9.04e+02 - 6.37e-01 8.98e-04h 1
68 2.4771237e+04 3.28e+03 3.62e+02 -8.6 9.03e+02 - 6.52e-01 6.37e-01h 1
69 2.2863803e+04 1.97e+03 2.33e+02 -8.6 3.28e+02 - 9.40e-01 4.00e-01h 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
70 2.0069449e+04 4.78e+01 5.64e+00 -8.6 1.97e+02 - 9.71e-01 9.76e-01h 1
71 2.0000010e+04 6.84e-03 8.52e-04 -8.6 4.78e+00 - 1.00e+00 1.00e+00h 1
72 2.0000000e+04 1.35e-13 6.10e-10 -8.6 6.95e-04 - 1.00e+00 1.00e+00h 1
73 2.0000000e+04 1.42e-14 6.61e-14 -8.6 2.77e-11 - 1.00e+00 1.00e+00h 1
Number of Iterations....: 73
(scaled) (unscaled)
Objective...............: -2.0000000046430814e+04 2.0000000046430814e+04
Dual infeasibility......: 6.6055616324458369e-14 6.6055616324458369e-14
Constraint violation....: 1.4210854715202004e-14 1.4210854715202004e-14
Variable bound violation: 3.9995490652700028e-07 3.9995490652700028e-07
Complementarity.........: 2.5441766264890888e-09 2.5441766264890888e-09
Overall NLP error.......: 2.2463137571874334e-09 2.5441766264890888e-09
Number of objective function evaluations = 149
Number of objective gradient evaluations = 71
Number of equality constraint evaluations = 149
Number of inequality constraint evaluations = 149
Number of equality constraint Jacobian evaluations = 75
Number of inequality constraint Jacobian evaluations = 75
Number of Lagrangian Hessian evaluations = 73
Total seconds in IPOPT = 0.029
EXIT: Optimal Solution Found.MIP solution
It is also possible to solve such problem by using a MIP formulation. The main issue is the product of variable in the upper level objective. However, this can be easily handled by using the package QuadraticToBinary.jl for automatic binary expansions. Because binary expansions require bounds on variables, we add the following lines:
set_lower_bound(lambda, 0.0)
set_upper_bound(lambda, 1000.0)Then, as before, we set a solver (now HiGHS with the QuadraticToBinary.jl wrapper) and a solution method (now Fortuny-Amat and McCarl):
set_optimizer(model,
()->QuadraticToBinary.Optimizer{Float64}(HiGHS.Optimizer()))
BilevelJuMP.set_mode(model,
BilevelJuMP.FortunyAmatMcCarlMode(dual_big_M = 100))
optimize!(model)Running HiGHS 1.5.1 [date: 1970-01-01, git hash: 93f1876e4]
Copyright (c) 2023 HiGHS under MIT licence terms
Presolving model
71 rows, 69 cols, 193 nonzeros
Presolve: Infeasible
Solving report
Status Infeasible
Primal bound -inf
Dual bound inf
Gap 0% (tolerance: 0.01%)
Solution status -
Timing 0.00 (total)
0.00 (presolve)
0.00 (postsolve)
Nodes 0
LP iterations 0 (total)
0 (strong br.)
0 (separation)
0 (heuristics)
ERROR: No invertible representation for getDualRayMore on DualOf usage
You might have a problem where you want duals of a vector of constraints like:
@constraint(Lower(model), reserves[i=1:3], (40 - gR1) + (40 - gR2) == 10 * i)3-element Vector{ConstraintRef{BilevelModel, Int64, ScalarShape}}:
reserves[1] : -gR1 - gR2 = -70
reserves[2] : -gR1 - gR2 = -60
reserves[3] : -gR1 - gR2 = -50then you can do
@variable(Upper(model), reserve_dual[i=1:3], DualOf(reserves[i]))3-element Vector{BilevelVariableRef}:
reserve_dual[1]
reserve_dual[2]
reserve_dual[3]or use anonymous variables
my_duals = []
for i in 1:3
var = @variable(Upper(model), variable_type = DualOf(reserves[i]))
push!(my_duals, var)
end
my_duals # a vector of anonimous variables3-element Vector{Any}:
anon
anon
anonThis page was generated using Literate.jl.