-
-
Notifications
You must be signed in to change notification settings - Fork 394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] [docs] Complex number AC Optimal Power Flow tutorial #3382
Closed
Closed
Changes from 7 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
35aa639
Initial draft
jd-foster 0b3174a
Merge branch 'master' into jdf/docs-opf
jd-foster 736427d
First working version
jd-foster 35b4ee1
Formatting check
jd-foster 6a0dff0
Address review comments
jd-foster a165885
Small fixes
jd-foster 449bbe4
Rename: - to _
jd-foster f13f927
Respond to changes, clarify wording
jd-foster 861a68c
Merge branch 'master' into jdf/docs-opf
jd-foster ee2a272
Formatting changes
odow da94cbe
Draft SDP theory section
jd-foster 30e7d2d
Implementation (but still testing solvers)
jd-foster File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors #src | ||
# This Source Code Form is subject to the terms of the Mozilla Public License #src | ||
# v.2.0. If a copy of the MPL was not distributed with this file, You can #src | ||
# obtain one at https://mozilla.org/MPL/2.0/. #src | ||
|
||
# # Optimal power flow | ||
|
||
# *This tutorial was originally contributed by James Foster (@jd-foster).* | ||
|
||
# This tutorial formulates and solves an optimal power flow problem, | ||
# a much-studied nonlinear problem from the field of electrical engineering. | ||
# Our particular focus is on obtaining a good estimate of the objective value | ||
# at the global optimum through the use of semidefinite programming techniques. | ||
|
||
# The tutorial highlights JuMP's ability to formulate problems involving | ||
# complex-valued decision variables and complex matrix cones such as the | ||
# [`HermitianPSDCone`](@ref) object. | ||
# See the [Complex number support](@ref) section of the manual for more details. | ||
|
||
# For another example of modeling with complex decision variables | ||
# see the [Quantum state discrimination](@ref) tutorial. | ||
|
||
# ## Required packages | ||
|
||
# This tutorial requires the following packages: | ||
|
||
using JuMP | ||
import Ipopt | ||
import SCS | ||
import LinearAlgebra | ||
import SparseArrays | ||
import DataFrames | ||
import Test | ||
|
||
# ## Formulation | ||
|
||
# Optimal power flow problems for electrical transmission typically pose the | ||
# following question: what is the most cost-effective operation of electricity | ||
# generators while meeting constraints on the safe limits of network components? | ||
|
||
# We will use the 9 _bus_ network test case `case9mod` to explore this problem | ||
# following the example of Krasko and Rebennack (2017). | ||
|
||
# Here *bus* and network *node* are taken as analogous terms, as are *branch* and transmission *line*. | ||
|
||
# The graph of the network shows three nodes each used for the different purposes of | ||
# generation ``G`` (nodes 1, 2 and 3), | ||
# trans-shipment (nodes 4, 6 and 8) | ||
# and demand ``L`` (5, 7, and 9). | ||
|
||
# ![Nine Nodes](../../assets/case9mod.png) | ||
|
||
# For future reference, let's name the number of nodes in the network: | ||
N = 9 | ||
|
||
# The network data can be summarised using a small number of arrays. | ||
# With the `sparsevec` function from the `SparseArrays` standard library package | ||
# we can just give the indices and values of the non-zero data points: | ||
|
||
# _Generation power lower (`lb`) and upper (`ub`) bounds (active and reactive)_ | ||
P_Gen_lb = SparseArrays.sparsevec([1, 2, 3], [10, 10, 10], N) | ||
P_Gen_ub = SparseArrays.sparsevec([1, 2, 3], [250, 300, 270], N) | ||
|
||
Q_Gen_lb = SparseArrays.sparsevec([1, 2, 3], [-5, -5, -5], N) | ||
Q_Gen_ub = SparseArrays.sparsevec([1, 2, 3], [300, 300, 300], N) | ||
|
||
# _Power demand levels (active, reactive and complex form)_ | ||
P_Demand = SparseArrays.sparsevec([5, 7, 9], [54, 60, 75], N) | ||
Q_Demand = SparseArrays.sparsevec([5, 7, 9], [18, 21, 30], N) | ||
S_Demand = P_Demand + im * Q_Demand | ||
|
||
# The key decision variables here are the real power injections ``P^G`` and | ||
# reactive power injections ``Q^G``over the allowed range of the generators. | ||
# All other buses must restrict their generation variables to 0. | ||
# On the other hand these non-generator nodes have a fixed | ||
# real and reactive power demand, denoted ``P^L`` and ``Q^L`` respectively | ||
# (fixed at 0 in the case of trans-shipment and generator nodes). | ||
|
||
# The cost of operating each generator is here modeled as a quadratic function of its real power output; | ||
# in our specific test case, the objective function to minimize is | ||
# ```math | ||
# \begin{align} | ||
# \min \;\; & \; 0.11 \;\; (P^G_1)^2 + 5 P^G_1 + 150 \\ | ||
# + \;\; & 0.085 \; (P^G_2)^2 + 1.2 P^G_2 + 600 \\ | ||
# \;\; & 0.1225 \; (P^G_3)^2 + P^G_3 + 335 \\ | ||
# \end{align} | ||
# ``` | ||
# Let's create a basic JuMP model with these settings: | ||
model = Model(Ipopt.Optimizer) | ||
set_silent(model) | ||
|
||
@variable(model, P_Gen_lb[i] <= P_G[i = 1:N] <= P_Gen_ub[i]) | ||
|
||
@objective( | ||
model, | ||
Min, | ||
0.11 * P_G[1]^2 + | ||
5 * P_G[1] + | ||
150 + | ||
0.085 * P_G[2]^2 + | ||
1.2 * P_G[2] + | ||
600 + | ||
0.1225 * P_G[3]^2 + | ||
P_G[3] + | ||
335 | ||
) | ||
|
||
# Even before solving, we can substitute the lower bound on each generator's real power range | ||
# (all 10, as it turns out in this case) | ||
|
||
objval_basic_lb = value(lower_bound, objective_function(model)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's unclear why this is a lower bound since you have quadratic terms |
||
println("Objective value (basic lower bound): $(objval_basic_lb)") | ||
|
||
# to see that we can do no better than an objective cost of 1188.75. | ||
|
||
# However, we have additional power flow constraints to satisfy. | ||
# In fact, we can get a quick but even better estimate from the direct observation that the | ||
# real power generated must meet or exceed the real power demand. | ||
|
||
@constraint(model, sum(P_G) >= sum(P_Demand)) | ||
optimize!(model) | ||
|
||
objval_better_lb = round(objective_value(model); digits = 2) | ||
println("Objective value (better lower bound): $(objval_better_lb)") | ||
|
||
# Power must flow from one or more generation nodes through the transmission lines | ||
# and end up at a demand node. The state variables of our steady-state alternating current (AC) | ||
# electrical network are *complex-valued* voltage variables ``V_1, \ldots, V_9``. Voltages capture both a magnitude and phase | ||
# of the node's electrical state in relation to the rest of the system. | ||
# An AC power system also extends the notion of resistance in wires found in a direct current (DC) circuit to a | ||
# complex quantity, known as the *impedance*, of each transmission line. | ||
# The reciprocal of impedance is known as *admittance*. | ||
# Together these complex quantities are used to express a complex version of *Ohm's law*: current flow | ||
# through a line is proportional to the difference in voltages on each end of the line | ||
# multiplied by the admittance. | ||
|
||
# ## Data | ||
|
||
# Let's assemble the data we need for writing the complex power flow constraints. | ||
# The data for the problem consists of a list of the real and imaginary parts of the line impedance. | ||
# We obtain from the `case9mod` MATPOWER test case `branch data` the following data table: | ||
df_br = DataFrames.DataFrame([ | ||
(1, 4, 0.0, 0.0576, 0.0), | ||
(4, 5, 0.017, 0.092, 0.158), | ||
(6, 5, 0.039, 0.17, 0.358), | ||
(3, 6, 0.0, 0.0586, 0.0), | ||
(6, 7, 0.0119, 0.1008, 0.209), | ||
(8, 7, 0.0085, 0.072, 0.149), | ||
(2, 8, 0.0, 0.0625, 0.0), | ||
(8, 9, 0.032, 0.161, 0.306), | ||
(4, 9, 0.01, 0.085, 0.176), | ||
]); | ||
# Let's make this into a more accessible `DataFrame`: | ||
DataFrames.rename!(df_br, [:F_BUS, :T_BUS, :BR_R, :BR_X, :BR_Bc]) | ||
|
||
# The first two columns describe the network, supplying the *from* and *to* connection points of the lines. | ||
# The last three columns give the branch resistance, branch reactance and *line-charging susceptance*. | ||
|
||
# We will also need to reference the `baseMVA` number (used for re-scaling): | ||
baseMVA = 100; | ||
# and the number of lines: | ||
M = size(df_br, 1) | ||
|
||
# From the first two columns of the branch data table, | ||
# we can create a sparse [incidence matrix](https://en.wikipedia.org/wiki/Incidence_matrix) | ||
# that simplifies handling of the network layout: | ||
A = | ||
SparseArrays.sparse(df_br.F_BUS, 1:M, 1, N, M) + | ||
SparseArrays.sparse(df_br.T_BUS, 1:M, -1, N, M) | ||
|
||
# We form the network impedance vector | ||
z = (df_br.BR_R .+ im * df_br.BR_X) / baseMVA | ||
# and the branch line-charging susceptance | ||
y_sh = 1 / 2 * (im * df_br.BR_Bc) * baseMVA | ||
# and then the *bus admittance* matrix is defined as | ||
Y = | ||
A * SparseArrays.spdiagm(1 ./ z) * A' + SparseArrays.spdiagm( | ||
LinearAlgebra.diag(A * SparseArrays.spdiagm(y_sh) * A'), | ||
) | ||
|
||
# (The second term looks more complicated because we only want to add the diagonal elements in the calculation; | ||
# the line-charging is used only in the nodal voltage terms and not the line voltage terms.) | ||
|
||
# ## JuMP model | ||
|
||
# Now we're ready to write the complex power flow constraints we need | ||
# to more accurately the electricity system. | ||
|
||
# We'll introduce a number of constraints that model both the physics and operational requirements. | ||
|
||
# Let's start by initializing a new model: | ||
model = Model(Ipopt.Optimizer) | ||
set_silent(model) | ||
|
||
# **Generation** | ||
|
||
# Create the nodal power generation variables: | ||
@variable(model, S_G[1:N] in ComplexPlane()) | ||
P_G = real(S_G); | ||
Q_G = imag(S_G); | ||
|
||
# Generators should operate over a prescribed range: | ||
@constraint(model, [i = 1:N], P_Gen_lb[i] <= P_G[i] <= P_Gen_ub[i]) | ||
@constraint(model, [i = 1:N], Q_Gen_lb[i] <= Q_G[i] <= Q_Gen_ub[i]) | ||
|
||
# **Demand** | ||
|
||
# Create the nodal power demand variables: | ||
@variable(model, S_D[1:N] in ComplexPlane()) | ||
|
||
# The loads in this model are assumed to be fixed and of constant-power type: | ||
@constraint(model, S_D .== S_Demand) | ||
|
||
# We need the system state variables, complex nodal voltages: | ||
@variable(model, V[1:N] in ComplexPlane(), start = 1.0 + 0.0im) | ||
|
||
# Operational constraints for maintaining voltage magnitude levels: | ||
@constraint(model, [i = 1:N], 0.9^2 <= real(V[i])^2 + imag(V[i])^2 <= 1.1^2) | ||
|
||
# Fixing the imaginary component of a _slack bus_ to zero sets its complex voltage angle to 0, | ||
# which serves as an origin or reference value for all other complex voltage angles. | ||
# Here we're using node 1 as the nominated slack bus: | ||
@constraint(model, imag(V[1]) == 0) | ||
|
||
# **Power flow constraints** | ||
|
||
# The current at a node is an expression representing a generalised version of Ohm's law and | ||
# [Kirchhoff's circuit laws](https://en.wikipedia.org/wiki/Kirchhoff%27s_circuit_laws): | ||
I_Node = Y * V | ||
|
||
# This next expression represents the power exchanged with the network from each node. | ||
# It is the product of nodal voltage and current but in its generalised complex form here: | ||
S_Node = LinearAlgebra.diagm(V) * conj(I_Node) | ||
|
||
# The power flow equations express a conservation of energy (power) principle, where | ||
# power generated less the power consumed must balance the power exchanged with the network: | ||
@constraint(model, S_G - S_D .== S_Node) | ||
|
||
# **Objective** | ||
|
||
# Quadratic cost of real power (as above): | ||
@objective( | ||
model, | ||
Min, | ||
0.11 * P_G[1]^2 + | ||
5 * P_G[1] + | ||
150 + | ||
0.085 * P_G[2]^2 + | ||
1.2 * P_G[2] + | ||
600 + | ||
0.1225 * P_G[3]^2 + | ||
P_G[3] + | ||
335 | ||
) | ||
|
||
optimize!(model) | ||
Test.@test isapprox(objective_value(model), 3087.84, atol = 1e-2) #src | ||
solution_summary(model) | ||
println( | ||
"Objective value (feasible solution): $(round(objective_value(model), digits=2))", | ||
) | ||
|
||
# We can see the voltage state variable solution: | ||
DataFrames.DataFrame(; | ||
Bus = 1:N, | ||
Magnitude = round.(abs.(value.(V)), digits = 2), | ||
AngleDeg = round.(rad2deg.(angle.(value.(V))), digits = 2), | ||
) | ||
|
||
# ## Relaxations and better objective bounds | ||
# The IPOPT solver uses an interior-point algorithm. It has local optimality guarantees, but is unable to certify | ||
# whether the solution is globally optimal. The solution we found is indeed globally optimal. | ||
# The work to verify this has been done in Bukhsh et al. (2013) and Krasko and Rebennack (2017), | ||
# and different solvers (such as Gurobi, SCIP and GLOMIQO) are also able to verify this. | ||
|
||
# The techniques of *convex relaxations* can also be used to improve on our current best lower bound: | ||
println("Objective value (better lower bound): $(objval_better_lb)") | ||
|
||
# ## References and further resources | ||
|
||
# **Krasko**, Vitaliy, and Steffen **Rebennack**. | ||
# [_Chapter 15: Global Optimization: Optimal Power Flow Problem._](https://doi.org/10.1137/1.9781611974683.ch15) | ||
# In Advances and Trends in Optimization with Engineering Applications, 187–205. MOS-SIAM Series on Optimization. | ||
# Society for Industrial and Applied Mathematics, 2017. | ||
|
||
# **Bukhsh**, W. A., Grothey, A., McKinnon, K. I., & Trodden, P. A. | ||
# [_Local solutions of the optimal power flow problem._](https://doi.org/10.1109/TPWRS.2013.2274577) | ||
# IEEE Transactions on Power Systems, 28(4), 4780-4788 (2013). | ||
|
||
# [**Test case `case9mod`**](https://www.maths.ed.ac.uk/optenergy/LocalOpt/9busnetwork.html): | ||
# from the | ||
# [Test Case Archive of Optimal Power Flow (OPF) Problems with Local Optima](https://www.maths.ed.ac.uk/optenergy/LocalOpt/). | ||
|
||
# **MATPOWER data format**: | ||
# [MATPOWER manual](https://matpower.org/docs/MATPOWER-manual.pdf): see especially Appendix B "Data File Format" and Table B-3. | ||
|
||
# **PowerModels.jl**: | ||
# The Julia/JuMP package [PowerModels.jl](https://lanl-ansi.github.io/PowerModels.jl/stable/) provides | ||
# an interface to a wide range of power flow formulations along with utilities for working with detailed network data. | ||
|
||
# **IPOPT solver**: | ||
# Wächter, A., Biegler, L. | ||
# [_On the implementation of an interior-point filter line-search algorithm for large-scale nonlinear programming._](https://doi.org/10.1007/s10107-004-0559-y) | ||
# Math. Program. 106, 25–57 (2006). |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In there, it seems "case9mod" is some standard name but it's only where from. Is it coming from some benchmarking suite ?