The function instantonAnalysis
handles several varieties of the instanton problem:
instantonAnalysis(psData; method="lagrange",
constraintType="activeFlow",
genResponse="droop")
The three keyword arguments determine which instanton analysis method to call.
Let's use Ipopt to solve the instanton problem with active power flows and proportional conventional generator response. First, make sure all necessary packages are available:
In [ ]:
# Install and update all required packages:
Pkg.add("MAT")
Pkg.add("Ipopt")
Pkg.add("JuMP")
Pkg.add("GraphViz")
Pkg.update()
Now make sure the julia-code
directory is beneath the current directory:
In [ ]:
readdir("julia-code")
Now we include instanton Julia code:
In [ ]:
# Include Julia code:
include("julia-code/instanton.jl");
Load power system data from a MATLAB .mat file into an instance of custom powerSystemData
type, then perform instanton analysis using JuMP with Ipopt:
In [ ]:
psData = psDataLoad()
In [ ]:
results = instantonAnalysis(psData,
method="solver",
constraintType="activeFlow",
genResponse="droop")
To display results, import SVG display capability for the notebook, then call plotInstantonResults
:
In [ ]:
using PyCall
@pyimport IPython.display as IPd
eventIdx = 1 # index of extreme event to plot
name = "instPlot"
plotInstantonResults(eventIdx,
psData,
results,
plotType="graph",
plotName=name,
constraintType="currentFlow") # generate dot file
In [ ]:
# Generate and display directed graph:
run(`dot -Tsvg $(name).dot -o $(name).svg`)
IPd.SVG(filename="$(name).svg") # show SVG inline
In [ ]:
# Define a type to hold power system data:
type powerSystemData
Sb # Base complex power
f # Lines: "from"
t # Lines: "to"
r # Lines: resistance
x # Lines: reactance
b # Lines: susceptance
Y # Lines: admittance
bustype # Buses: type
Gp # Buses: conv. active gen
Gq # Buses: conv. reactive gen
Dp # Buses: active demand
Dq # Buses: reactive demand
Rp # Buses: wind active gen
Rq # Buses: wind reactive gen
Pmax # Buses: max. active gen
Pmin # Buses: min. active gen
Qmax # Buses: max. reactive gen
Qmin # Buses: min. reactive gen
Plim # Lines: flow limit
Vg # Buses: nominal voltage
Vceiling # Buses: max. voltage
Vfloor # Buses: min. voltage
busIdx # Buses: index
N # Buses: total
Nr # Buses: renewable
Ng # Buses: conventional
k # Buses: participation factors
end
Similarly, I define a type to store analysis results:
In [ ]:
type instantonResults
score
ρ
θ
α
constrIdx
Gp
end
Having stored all system data in a single object, I pass it to the following function, which builds a series of models, passes them to Ipopt, and returns solutions. Here is an abbreviated version of the code. In a moment we will break it down a few lines at a time.
In [ ]:
function solver_activeFlow_droop(psDL)
"""This function uses Ipopt to perform instanton analysis
for the case with conventional generator droop resopnse.
"""
# ...
# Enforce each constraint in the f --> t direction
for idx = 1:length(f)
(m, ρ, θ, α, PowerBalance, Slack, Congestion, Participation) = droopDCModel(idx, 1, Rp, Gp, Dp, f, t, x, Y, bustype, Plim,k)
status = solve(m);
push!(score, getObjectiveValue(m))
push!(result,status)
push!(theta, getValue(θ)[:])
push!(alpha, getValue(α))
push!(rho,getValue(ρ)[:])
# Compute conventional generation post-instanton:
push!(Gpost, Gp + k.*getValue(α))
end
# ...
function droopDCModel(idx, sense, Rp, Gp, Dp, f, t, x, Y, bustype, Plim,k)
# DROOP RESPONSE
# Create model saturating line 'idx' in direction 'sense' (±1)
# This function uses JuMP and Ipopt
m = Model(solver = IpoptSolver()) # Use Ipopt to solve model
N = length(Rp)
@defVar(m, ρ[1:N] >= 0) # Add decision variables to model (renewable gen)
@defVar(m, θ[1:N]) # Add bus angles
@defVar(m, α) # mismatch
setObjective(m, :Min, 0.5*sum([(ρ[i] - Rp[i])^2 for i in find(Rp)]))
# add power balance constraints:
@defConstrRef PowerBalance[1:N]
for i in setdiff(1:N,find(bustype.==3))
PowerBalance[i] = @addConstraint(m, sum([Y[i,j]*θ[j] for j in 1:N]) == Gp[i] + k[i]*α + ρ[i] - Dp[i])
end
@addConstraint(m, NonWind, sum([ρ[i] for i in setdiff(collect(1:N),find(Rp))]) == 0) # ρ=0 for non-wind nodes
@addConstraint(m, Slack, θ[find(bustype.==3)[1]] == 0) # θ = 0 for slack bus
@addConstraint(m, Congestion, θ[f[idx]] - θ[t[idx]] == sense*x[idx]*Plim[idx]) # saturate a line
@addConstraint(m, Participation, α == (sum(Dp) - sum([ρ[i] for i in find(Rp)])) - sum(Gp))
return m, ρ, θ, α, PowerBalance, Slack, Congestion, Participation
end
end
Let's concentrate on the solver model composed with JuMP macros.
m = Model(solver = IpoptSolver())
Define some variables:
N = length(Rp)
@defVar(m, ρ[1:N] >= 0) # Add decision variables
@defVar(m, θ[1:N]) # Add bus angles
@defVar(m, α) # mismatch
Compare with:
Set the objective:
setObjective(m, :Min, 0.5*sum([(ρ[i] - Rp[i])^2
for i in find(Rp)]))
Compare with: $$\min \frac{1}{2} \left( \rho - \rho_0 \right)^\top \left( \rho - \rho_0 \right)$$
Power balance constraints:
@defConstrRef PowerBalance[1:N]
for i in setdiff(1:N,find(bustype.==3))
PowerBalance[i] = @addConstraint(m,
sum([Y[i,j]*θ[j] for j in 1:N]) ==
Gp[i] + k[i]*α + ρ[i] - Dp[i])
end
Compare with: $$\begin{align} \sum_k( Y_{ik} \theta_k) &= G_{i} + \rho_i - D_{i} \quad \forall i \in N \\ G &= G_{0} + k\alpha \end{align}$$
Constrain renewable generation to 0 for non-wind nodes:
@addConstraint(m, NonWind,
sum([ρ[i] for i in setdiff(collect(1:N),find(Rp))]) == 0)
Define slack bus:
@addConstraint(m, Slack,
θ[find(bustype.==3)[1]] == 0)
Compare with: $$\theta_i = 0 \quad \text{ where bus $i$ is the angle reference} $$
Add "congestion constraint":
@addConstraint(m, Congestion,
θ[f[idx]] - θ[t[idx]] == sense*x[idx]*Plim[idx])
(Note that sense
is $\pm 1$.) Compare with:
$$ \theta_i - \theta_k = x_{ik} P_{lim,ik} \quad \text{for each } (i,k) \in G $$
Conventional generator droop response:
@addConstraint(m, Participation,
α == (sum(Dp) - sum([ρ[i] for i in find(Rp)])) - sum(Gp))
Compare with:
For each line (and direction) in the network, a model is generated and passed to Ipopt. The last line of the solver_activeFlow_droop
function returns solution information:
return m, ρ, θ, α, PowerBalance, Slack, Congestion, Participation
The plotInstantonResults()
function generates the graph we saw:
function plotInstantonResults( eventIdx,
psData,
results;
plotType="graph",
plotName="defaultPlotName",
constraintType="activeFlow")
Note the presence of positional and keyword arguments. If we want a visualization other than the graph, we can choose another value for plotType
and add code to the function to respond to it. The function is general enough to handle all plotting.
The graph output in particular uses a function I wrote called writeDot()
, which is about 70 lines of code and generates a .dot
file. Graphviz uses this file to render the network as a directed graph.
Here we ran Julia code to perform instanton analysis on the IEEE RTS-96 network. We stepped through the process of translating a mathematical programming model into Julia JuMP macros. We also plotted the results using Graphviz and discussed how this was implemented in Julia.
In the last notebook we will explore an extension to the instanton analysis presented here. Along the way we will see the usefulness of Julia and the IJulia notebook in exploration, prototyping, and presentation.