Gurobi.jl numerical trouble in optimal power flow

Hi. I am trying to model one of the ‘classic’ conic relaxations of the optimal powerflow model (see Branch Flow Model: Relaxations and Convexification—Part I)

The problem I am running into is that depending on the loads / size of the grid solving becomes very unreliable. I am using Gurobi. I will frequently see:

Error
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (win64 - Windows 11.0 (22631.2))

CPU model: AMD Ryzen 9 5950X 16-Core Processor, instruction set [SSE2|AVX|AVX2]
Thread count: 16 physical cores, 32 logical processors, using up to 32 threads

Optimize a model with 3264 rows, 3456 columns and 8064 nonzeros
Model fingerprint: 0x5ec5409d
Model has 384 quadratic constraints
Coefficient statistics:
  Matrix range     [1e+00, 7e+00]
  QMatrix range    [1e+00, 1e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [8e-01, 1e+00]
  RHS range        [1e-02, 1e+00]
Presolve removed 1920 rows and 1824 columns
Presolve time: 0.00s
Presolved: 1344 rows, 1728 columns, 4224 nonzeros
Presolved model has 384 second-order cone constraints
Ordering time: 0.00s

Barrier statistics:
 AA' NZ     : 6.528e+03
 Factor NZ  : 1.008e+04 (roughly 1 MB of memory)
 Factor Ops : 9.744e+04 (less than 1 second per iteration)
 Threads    : 1

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   1.70141978e+02 -1.24205980e+02  1.70e+00 7.06e-01  4.13e-01     0s
   1   1.70141978e+02 -1.24205980e+02  1.70e+00 7.06e-01  4.13e-01     0s
   2   2.00214585e+05 -1.83305591e+05  2.09e+03 1.00e+03  6.37e+05     0s
   3   2.00214585e+05 -1.83305591e+05  2.09e+03 1.00e+03  6.37e+05     0s
   4   2.00214585e+05 -1.83305591e+05  2.09e+03 1.00e+03  6.37e+05     0s
   5   2.00214585e+05 -1.83305591e+05  2.09e+03 1.00e+03  6.37e+05     0s

Barrier performed 5 iterations in 0.02 seconds (0.01 work units)
Numerical trouble encountered

Model may be infeasible or unbounded.  Consider using the
homogeneous algorithm (through parameter 'BarHomogeneous')

To improven numerical stability I have rescaled everything to p.u. dimensions. But since I am new to this there’s a chance I made a typo/mistake somewhere.

Here is the model code. I set up a small test grid of just 4 buses, 2 identical loads. The real problem has about 100 buses.

test network with 4 buses 
      0   (slack)
      |
      1   (junction)
     / \
    2   3 (loads)
Model
using JuMP
using Gurobi
using Clarabel
using Random


Random.seed!(333)

# p.u. base values
S_base = 1E5 # [VA]
V_base = 230.0 # [V]

Z_base = V_base^2 / S_base
I_base = S_base / V_base
S_max = 1E6 * 0.23 * 0.16 * 3 / S_base

# voltage constraints V_base ± 10% -> (207V, 253V) at 230V base
V_lb = 0.90 # [pu]
V_ub = 1.10

T = 1:96


function add_grid_model(;
    model::Model,
    T=1:96
)
    # test network with 4 buses 
    #       0   (slack)
    #       |
    #       1   (junction)
    #      / \
    #     2   3 (loads)

    B = [0, 1, 2, 3]
    H = [2, 3]
    L = [(0, 1), (1, 2), (1, 3)]

    bus_out = Dict(0 => [1], 1 => [2, 3], 2 => [], 3 => [])
    bus_in = Dict(0 => [], 1 => [0], 2 => [1], 3 => [1])

    # base loads 
    # P_base = 1e4 * ones(length(T)) / S_base
    # P_base = round.(1e3 .* abs.(randn(length(T))) / S_base .+ 3e4 / S_base, digits=3)
    P_base = 1e3 .* abs.(randn(length(T))) / S_base .+ 2e4 / S_base
    Q_base = zeros(length(T))

    # slack bus ID
    SB = 0

    # real and reactive line impedances
    R = Dict((i, j) => 0.1 / Z_base for (i, j) in L)
    X = Dict((i, j) => 0.1 / Z_base for (i, j) in L)
    Inom = Dict((i, j) => 1.0 for (i, j) in L)

    # variables
    @variables(model, begin
        # voltage squared eq. (7)
        V_lb^2 <= v[B, T] <= V_ub^2

        # bus power injections
        P[B, T], (base_name = "PBusInjection")
        Q[B, T], (base_name = "QBusInjection")

        # power flows
        P_line[L, T], (base_name = "PLineFlow")
        Q_line[L, T], (base_name = "QLineFlow")

        # line current
        0 <= I_line[L, T], (base_name = "CurrentSquare")
    end)

    # slack bus constraints
    @constraints(model, begin
        TrafoLimit[t in T], [S_max, P[SB, t], Q[SB, t]] in SecondOrderCone()
    end)

    # bus / line constraints
    @constraints(model, begin
        # real power balance eq. (1)
        RealPowerBalance[j in B, t in T],
        P[j, t] == sum(P_line[(j, k), t] for k in bus_out[j]) -
                   sum(P_line[(i, j), t] - R[(i, j)] * I_line[(i, j), t] for i in bus_in[j])

        # reactive power balance eq. (2)
        ReactivePowerBalance[j in B, t in T],
        Q[j, t] == sum(Q_line[(j, k), t] for k in bus_out[j]) -
                   sum(Q_line[(i, j), t] - X[(i, j)] * I_line[(i, j), t] for i in bus_in[j])

        # voltage drop eq. (3)
        VoltageDrop[(i, j) in L, t in T],
        v[j, t] == v[i, t] - 2 * (P_line[(i, j), t] * R[(i, j)] + Q_line[(i, j), t] * X[(i, j)]) +
                   (R[(i, j)]^2 + X[(i, j)]^2) * I_line[(i, j), t]

        # conic OPF eq. (4)
        ConicOPF[(i, j) in L, t in T],
        [
            I_line[(i, j), t] + v[i, t],
            2 * P_line[(i, j), t],
            2 * Q_line[(i, j), t],
            I_line[(i, j), t] - v[i, t],
        ] in SecondOrderCone()

        # line current limit eq. (6)
        LineCurrentLimit[(i, j) in L, t in T], I_line[(i, j), t] <= Inom[(i, j)]
    end)

    # load constraints
    @constraints(model, begin
        Transmission[i in B, t in T; i ∉ H && i ≠ SB], P[i, t] == 0
        RealBaseLoad[i in H, t in T], P[i, t] == -P_base[t]
        ReactiveBaseLoad[i in H, t in T], Q[i, t] == -Q_base[t]
    end)
end


# create the model
# model = Model(Clarabel.Optimizer)
model = Model(Gurobi.Optimizer)
set_attribute(model, "BarHomogeneous", 1)

# add grid model 
add_grid_model(model=model, T=T)

P = model[:P]

# define objective functions
@expressions(model, begin
    J_gen, sum(P[0, T])
end)

@objective(model, Min, J_gen)

# optimize the model and return it
optimize!(model)
@assert is_solved_and_feasible(model)

And here is some plotting code I used, in case it’s useful.

Plotting
using CairoMakie

function plot_line_currents(model::Model, lines::Vector{Tuple{Int,Int}}=[(34, 0), (2, 74)])
    fig = Figure(; size=(1000, 600))

    I_line = value.(model[:I_line])
    for (idx, (i, j)) in enumerate(lines)
        ax = Axis(fig[idx, 1], xlabel="Time [hours]",
            ylabel="Current [A]",
            title="($i -> $j)",
            xticks=(1:4:97, string.(0:1:24))
        )
        scatterlines!(ax, Vector(I_line[(i, j), :]), color=:blue, label="$i->$j", linewidth=2)
    end

    fig
end

function plot_pq(model::Model, bus::Int)
    fig = Figure(; size=(1000, 300))
    ax = Axis(fig[1, 1], xlabel="Time [hours]",
        ylabel="Power [p.u.]",
        title="Power flow",
        xticks=(1:4:97, string.(0:1:24))
    )

    # index of slack bus (transformer)
    # SB = argmin(value.(model[:P]).axes[2])
    P_trafo = Matrix(value.(model[:P]))[bus, :]
    Q_trafo = Matrix(value.(model[:Q]))[bus, :]


    # lines!(ax, sol[:P], color = :blue, label = "Transformer", linestyle = :so
    scatterlines!(ax, P_trafo, color=:blue, label="P trafo", linewidth=2)
    scatterlines!(ax, Q_trafo, color=:red, label="Q trafo", linewidth=2)
    axislegend(ax, position=:lb)

    # display
    fig
end

I have read pretty much everything in Guidelines for Numerical Issues - Gurobi Optimizer Reference Manual. Based on the advice there I rescaled everything to p.u. domain and the coefficient ranges are quite decent now, but the problems remain.

Any advice would be welcome. Also, if any PowerModels.jl users / experts are reading this please let me know if you see any mistakes in the formulations.

ps. I know I could use PowerModels.jl instead of doing this myself, but this is a better learning experience.

For testing purposes I also ran the model using Clarabel.jl, the only change to the code being:

model = Model(Clarabel.Optimizer)

The Clarabel log, in case anyone spots something suspicious:

-------------------------------------------------------------
           Clarabel.jl v0.9.0  -  Clever Acronym
                   (c) Paul Goulart
                University of Oxford, 2022
-------------------------------------------------------------

problem:
  variables     = 2016
  constraints   = 4320
  nnz(P)        = 0
  nnz(A)        = 7680
  cones (total) = 387
    : Zero        = 1,  numel = 1536
    : Nonnegative = 2,  numel = (672,672)
    : SecondOrder = 384,  numel = (3,3,3,3,...,4)

settings:
  linear algebra: direct / qdldl, precision: Float64
  max iter = 200, time limit = Inf,  max step = 0.990
  tol_feas = 1.0e-08, tol_gap_abs = 1.0e-08, tol_gap_rel = 1.0e-08,
  static reg : on, ϵ1 = 1.0e-08, ϵ2 = 4.9e-32
  dynamic reg: on, ϵ = 1.0e-13, δ = 2.0e-07
  iter refine: on, reltol = 1.0e-13, abstol = 1.0e-12,
               max iter = 10, stop ratio = 5.0
  equilibrate: on, min_scale = 1.0e-04, max_scale = 1.0e+04
               max iter = 10

iter    pcost        dcost       gap       pres      dres      k/t        μ       step
---------------------------------------------------------------------------------------------
  0   4.7111e+01  -7.3618e+02  1.66e+01  5.89e-01  3.78e-01  1.00e+00  1.79e+00   ------
  1   5.3901e+01  -6.3766e+01  2.18e+00  1.44e-01  6.25e-02  2.55e-01  5.40e-01  8.50e-01
  2   4.6217e+01   3.4599e+01  3.36e-01  1.64e-02  8.08e-03  5.80e-02  8.34e-02  8.87e-01
  3   4.4820e+01   4.2589e+01  5.24e-02  3.18e-03  1.56e-03  9.80e-03  1.68e-02  8.34e-01
  4   4.4579e+01   4.4241e+01  7.63e-03  4.70e-04  2.34e-04  1.19e-03  2.54e-03  8.90e-01
  5   4.4573e+01   4.4548e+01  5.67e-04  3.52e-05  1.74e-05  8.60e-05  1.91e-04  9.28e-01
  6   4.4573e+01   4.4572e+01  2.05e-05  1.27e-06  6.28e-07  1.32e-06  6.87e-06  9.90e-01
  7   4.4573e+01   4.4573e+01  5.93e-07  3.68e-08  1.82e-08  3.82e-08  1.99e-07  9.71e-01  
  8   4.4573e+01   4.4573e+01  2.99e-08  1.85e-09  9.16e-10  1.92e-09  1.00e-08  9.50e-01
  9   4.4573e+01   4.4573e+01  1.70e-09  1.05e-10  5.20e-11  1.04e-10  5.69e-10  9.48e-01
---------------------------------------------------------------------------------------------
Terminated with status = solved
solve time = 17.7ms

Oddly enough Clarabel has no issues with this model at all. I didn’t expect Clarabel to succeed where Gurobi fails.

Model may be infeasible or unbounded. Consider using the
homogeneous algorithm (through parameter ‘BarHomogeneous’)

Did you try this suggestion?

1 Like

Yep. I edited the code to reflect that. I also fixed the random number generator to a seed that will always cause Gurobi to fail.

I tested this a bit earlier, it’s only reproducible on windows/mac (tested over 500 seeds on linux). For now, the best I could come up with is setting Presolve=0, please test this. But this won’t be great for larger models.

2 Likes

Very odd. Thank you for looking into this.

set_attribute(model, "Presolve", 0) seems to work for both the larger and the smaller model.

I am still not sure where the numerical instability is coming from though.