Chance-Constrained DC Optimal Power Flow
The purpose of this tutorial is to show how polynomial chaos can be leveraged to solve optimization problems under uncertainty. Specifically, we study chance-constrained DC optimal power flow as it is presented in this paper.
We consider the following 4-bus system that has a total of two generators (buses 1 and 3) and two loads (buses 2 and 4):
We formalize the numbering of the generators (superscript $g$), loads (superscript $d$ for demand), and branches (superscript $br$) as follows
\[\mathcal{N}^g = \{ 1, 3\}, \, \mathcal{N}^d = \{ 2, 4\}, \, \mathcal{N}^{br} = \{ 1, 2, 3, 4, 5 \}.\]
With each, we associate a linear cost with cost coefficient $c_i$ for all $i \in \mathcal{N}^g$. Each generator must adhere to its engineering limits given by $(\underline{p}_i^g , \overline{p}_i^g )$ for all $i \in \mathcal{N}^g$. Also, each line is constrained by its limits $(\underline{p}_i^{br}, \overline{p}_i^{br})$ for all $i \in \mathcal{N}^{br}$.
We model the demand at the buses $i \in \mathcal{N}^d$ in terms of uniform distributions with known mean $\mu_i$ and standard deviation $\sigma_i$. We concisely write
\[\mathsf{p}_i^d \sim \mathsf{U}(\mu_i, \sigma_i) \quad \forall i \in \mathcal{N}^d.\]
For simplicity, we consider DC conditions. Hence, energy balance reads
\[\sum_{i \in \mathcal{N}^g} \mathsf{p}_i^g - \sum_{i \in \mathcal{N}^d} \mathsf{p}_i^d = 0,\]
and the vector of branch flows is computed from the power transfer distribution factor (PTDF) matrix $\Psi$ via
\[\mathsf{p}^{br} = \Psi (C^p\mathsf{p}^g + C^d\mathsf{p}^d).\]
The matrices $C^p$ and $C^d$ map the generators and the loads to the correct buses, respectively.
We want to solve a chance-constrained optimal power flow problem under DC conditions. According to this paper, we can formulate the problem as $\underset{\mathsf{p^{g}}}{\operatorname{min}} \, \sum_{i \in \mathcal{N}_g} c_i \mathbb{E}( \mathsf{p}_i^g)$ subject to
\[\sum_{i \in \mathcal{N}^g} \mathsf{p}_i^g - \sum_{i \in \mathcal{N}^d} \mathsf{p}_i^d = 0, \\ \underline{p}_i^g \leq \mathbb{E}(\mathsf{p}_i^g) \pm \lambda_i^g \sqrt{\mathbb{V}(\mathsf{p}_i^g)} \leq \overline{p}_i^g \forall i \in \mathcal{N}^g,\\ \underline{p}_i^{br} \leq \mathbb{E}(\mathsf{p}_i^{br}) \pm \lambda_i^{br} \sqrt{\mathbb{V}(\mathsf{p}_i^{br})} \leq \overline{p}_i^{br} \forall i \in \mathcal{N}^{br},\]
which minimizes the total expected generation cost subject to the DC power flow equations and chance-constrained engineering limits.
Let's solve the problem using PolyChaos
and JuMP
, using Mosek
as a solver.
using PolyChaos, JuMP, MosekTools, LinearAlgebra
Let's define system-specific quantities such as the incidence matrix and the branch flow parameters. From these, we can compute the PTDF matrix $\Psi$ (assuming the slack is at bus 1).
A = [-1 1 0 0; -1 0 1 0; -1 0 0 1; 0 1 -1 0; 0 0 -1 1] # incidence matrix
Nl, N = size(A, 1), size(A, 2)
Bbr = diagm(0 => -(2 .+ 10 * rand(Nl))) # line parameters
Ψ = [zeros(Nl) -Bbr * A[:, 2:end] * inv(A[:, 2:end]' * Bbr * A[:, 2:end])] # PTDF matrix
5×4 Matrix{Float64}:
0.0 -0.437908 -0.249446 -0.182227
0.0 -0.282016 -0.376572 -0.275096
0.0 -0.280076 -0.373982 -0.542677
0.0 -0.562092 0.249446 0.182227
0.0 0.280076 0.373982 -0.457323
Now we can continue the remaining ingredients that specify our systems:
Cp, Cd = [1 0; 0 0; 0 0; 0 1], [0 0; 1 0; 0 1; 0 0] # book-keeping
Ng, Nd = size(Cp, 2), size(Cd, 2)
c = 4 .+ 10 * rand(Ng) # cost function parameters
λp, λl = 1.6 * ones(Ng), 1.6 * ones(Nl) # lambdas for chance constraint reformulations
pmax, pmin = 10 * ones(Ng), zeros(Ng) # engineering limits
plmax, plmin = 10 * ones(Nl), -10 * ones(Nl) # engineering limits
([10.0, 10.0, 10.0, 10.0, 10.0], [-10.0, -10.0, -10.0, -10.0, -10.0])
We specify the uncertainty using PolyChaos
:
deg = 1
opq = [Uniform01OrthoPoly(deg; Nrec = 5 * deg), Uniform01OrthoPoly(deg; Nrec = 5 * deg)]
mop = MultiOrthoPoly(opq, deg)
Npce = mop.dim
3
It remains to specify the PCE coefficients, for which we will use convert2affine
.
d = zeros(Nd, Npce) # PCE coefficients of load
d[1, [1, 2]] = convert2affinePCE(1.0, 0.1, mop.uni[1], kind = "μσ")
d[2, [1, 3]] = convert2affinePCE(2.0, 0.2, mop.uni[2], kind = "μσ")
2-element Vector{Float64}:
2.0
0.6928203230275509
Now, let's put it all into an optimization problem, specifically a second-order cone program. To build the second-order cone constraints, we define a helper function buildSOC
.
function buildSOC(x::Vector, mop::MultiOrthoPoly)
t = [sqrt(Tensor(2, mop).get([i, i])) for i in 0:(mop.dim - 1)]
(t .* x)[2:end]
end
buildSOC (generic function with 1 method)
Finally, let's use JuMP
to formulate and then solve the problem. We use Mosek
to solve the problem; for academic use there are free license.
model = Model(with_optimizer(Mosek.Optimizer))
@variable(model, p[i in 1:Ng, j in 1:Npce], base_name="p")
@constraint(model, energy_balance[j in 1:Npce],
sum(p[i, j] for i in 1:Ng) - sum(d[i, j] for i in 1:Nd)==0)
@constraint(model, con_pmax[i in 1:Ng],
[1 / λp[i] * (pmax[i] - mean(p[i, :], mop)); buildSOC(p[i, :], mop)] in SecondOrderCone())
@constraint(model, con_pmin[i in 1:Ng],
[1 / λp[i] * (mean(p[i, :], mop) - pmin[i]); buildSOC(p[i, :], mop)] in SecondOrderCone())
pl = Ψ * (Cp * p + Cd * d)
@constraint(model, con_plmax[i in 1:Nl],
[1 / λl[i] * (plmax[i] - mean(pl[1, :], mop)); buildSOC(pl[i, :], mop)] in SecondOrderCone())
@constraint(model, con_plmin[i in 1:Nl],
[1 / λl[i] * (mean(pl[1, :], mop) - plmin[i]); buildSOC(pl[i, :], mop)] in SecondOrderCone())
@objective(model, Min, sum(mean(p[i, :], mop) * c[i] for i in 1:Ng))
optimize!(model) # here we go
Let's extract the numerical values of the optimal solution.
@assert termination_status(model)==MOI.OPTIMAL "Model not solved to optimality."
psol, plsol, obj = value.(p), value.(pl), objective_value(model)
Great, we've solved the problem. How do we now make sense of the solution? For instance, we can look at the moments of the generated power:
p_moments = [[mean(psol[i, :], mop) var(psol[i, :], mop)] for i in 1:Ng]
Similarly, we can study the moments for the branch flows:
pbr_moments = [[mean(plsol[i, :], mop) var(plsol[i, :], mop)] for i in 1:Nl]