Batch power flow
ExaPF provides a way to evaluate the expressions by blocks, opening the way to introduce more parallelism in the code.
BlockPolarForm
We recall that a given NetworkStack
stack
stores the different variables and parameters (power generations, voltages, loads) required to evaluate the power flow model.
stack = ExaPF.NetworkStack(polar);
21-elements NetworkStack{Vector{Float64}}
The variables are stored in the field stack.input
, the parameters in the field stack.params
. The parameters encode the active pd
and reactive loads qd
at all buses in the network, such that
nbus = ExaPF.get(polar, PS.NumberOfBuses());
pd = stack.params[1:nbus]
qd = stack.params[nbus+1:2*nbus]
9-element Vector{Float64}:
0.0
0.0
0.0
0.0
0.3
0.0
0.35
0.0
0.5
By default, a NetworkStack
stores one set of loads $p_0$.
Suppose now we want to evaluate the model associated with the polar formulation for $N$ different set of parameters (=scenarios) $p_1, \cdots, p_N$. ExaPF allows to streamline the polar formulation with a BlockPolarForm
structure:
nscen = 10;
blk_polar = ExaPF.BlockPolarForm(polar, nscen)
10-BlockPolar formulation (instantiated on device CPU(false))
Network characteristics:
#buses: 9 (#slack: 1 #PV: 2 #PQ: 6)
#generators: 3
#lines: 9
giving a mathematical formulation with:
#controls: 5
#states : 14
Then, ExaPF can also instantiate a NetworkStack
object, with the memory required to store the variables of the different scenarios:
blk_stack = ExaPF.NetworkStack(blk_polar)
210-elements NetworkStack{Vector{Float64}}
We can pass the scenarios manually using the function set_params!
:
ploads = rand(nbus, nscen);
qloads = rand(nbus, nscen);
ExaPF.set_params!(blk_stack, ploads, qloads)
90-element Vector{Float64}:
0.1699293533325139
0.028172874195840802
0.6450961255328432
0.7158460505593955
0.6619812898864839
0.35227542049345584
0.01880091836544795
0.5695242795245709
0.4145832411803315
0.7266099079798154
⋮
0.33193888968635277
0.20661371720091937
0.6852132582458119
0.5657342800342086
0.6465526657916233
0.8767878131601121
0.04567774872711505
0.8418571270458194
0.43650781749660095
The structure blk_stack
stores $N$ different realizations for the variables stored in the field input
(vmag
, vang
and pgen
). By default, the initial values are set according to the values specified in blk_polar
(usually defined when importing the data from the instance file):
reshape(blk_stack.vmag, nbus, nscen)
9×10 Matrix{Float64}:
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
Only the parameters are varying according to the scenarios we passed as input in the constructor:
reshape(blk_stack.pload, nbus, nscen)
9×10 Matrix{Float64}:
0.405433 0.467973 0.668551 … 0.300926 0.979249 0.202288
0.474281 0.632257 0.131697 0.0818458 0.249237 0.139083
0.0864773 0.406815 0.665136 0.21748 0.145597 0.90734
0.512252 0.957117 0.834536 0.967073 0.593285 0.875167
0.823264 0.323997 0.787562 0.00635273 0.909702 0.169815
0.208514 0.496248 0.910327 … 0.750124 0.586277 0.333802
0.601783 0.31134 0.177645 0.182579 0.37754 0.00430702
0.681859 0.131662 0.0513694 0.259726 0.971999 0.128388
0.753585 0.746461 0.45151 0.247158 0.838462 0.580973
Evaluate expressions in block
ExaPF takes advantage of the block structure when using a BlockPolarForm
.
As an example, suppose we want to evaluate the power flow balances in block form with a PowerFlowBalance
expression:
powerflow = ExaPF.PowerFlowBalance(blk_polar) ∘ ExaPF.PolarBasis(blk_polar);
ExaPF.ComposedExpressions{ExaPF.PolarBasis{Vector{Int64}, SparseArrays.SparseMatrixCSC{Float64, Int64}}, ExaPF.PowerFlowBalance{Vector{Float64}, SparseArrays.SparseMatrixCSC{Float64, Int64}}}(PolarBasis (AbstractExpression), PowerFlowBalance (AbstractExpression))
A block evaluation takes as input the NetworkStack
blk_stack
structure:
m = div(length(powerflow), nscen);
blk_output = zeros(m * nscen);
powerflow(blk_output, blk_stack); # inplace evaluation
reshape(blk_output, m, nscen)
14×10 Matrix{Float64}:
-1.15572 -0.997743 -1.4983 … -1.54815 -1.38076 -1.49092
-0.763523 -0.443185 -0.184864 -0.63252 -0.704403 0.0573403
0.512252 0.957117 0.834536 0.967073 0.593285 0.875167
0.823264 0.323997 0.787562 0.00635273 0.909702 0.169815
0.208514 0.496248 0.910327 0.750124 0.586277 0.333802
0.601783 0.31134 0.177645 … 0.182579 0.37754 0.00430702
0.681859 0.131662 0.0513694 0.259726 0.971999 0.128388
0.753585 0.746461 0.45151 0.247158 0.838462 0.580973
0.548846 0.427701 -0.101785 0.65755 0.694674 0.398734
0.403981 0.136941 -0.140736 0.543757 0.307107 0.388553
0.0687754 -0.0666554 0.382083 … -0.120536 0.197425 0.593288
-0.160199 0.658388 0.618887 0.598191 0.782261 -0.133322
0.342024 -0.0435061 -0.0647087 0.338816 0.193881 0.614357
0.173583 0.297709 0.281843 -0.141589 -0.042818 0.195508
We get $N$ different results for the power flow balance equations, depending on which scenario we are on.
Solve power flow in block on the CPU
Once the different structures used for block evaluation instantiated, one is able to solve the power flow in block on the CPU using the same function nlsolve!
. The block Jacobian is evaluated with automatic differentiation using a ArrowheadJacobian
structure:
blk_jx = ExaPF.ArrowheadJacobian(blk_polar, powerflow, State());
blk_jx.J
140×140 SparseArrays.SparseMatrixCSC{Float64, Int64} with 820 stored entries:
⎡⡱⣮⡲⣞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⣸⢮⣻⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⡱⣮⡲⣞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⣸⢮⣻⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡶⣝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⣜⢯⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡶⣝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣜⢯⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡶⣝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣜⢯⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⣺⣜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣚⢾⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⣺⣜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣚⢾⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⣺⣜⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣚⢾⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡺⣕⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢞⢮⡿⣯⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡺⣕⎥
⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢞⢮⡿⣯⎦
We notice that the ArrowheadJacobian
computes the resulting Jacobian as a block diagonal matrix. The ArrowheadJacobian
has a slightly different behavior than its classical counterpart AutoDiff.Jacobian
, in the sense that one has to pass the parameters manually to initiate internally the dual numbers:
ExaPF.set_params!(blk_jx, blk_stack);
ExaPF.jacobian!(blk_jx, blk_stack);
140×140 SparseArrays.SparseMatrixCSC{Float64, Int64} with 820 stored entries:
⎡⡱⣮⡲⣞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⣸⢮⣻⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⡱⣮⡲⣞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⣸⢮⣻⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡶⣝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⣜⢯⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡶⣝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣜⢯⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡶⣝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣜⢯⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⣺⣜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣚⢾⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⣺⣜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣚⢾⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⣺⣜⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣚⢾⡻⣮⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡺⣕⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢞⢮⡿⣯⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡵⣯⡺⣕⎥
⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢞⢮⡿⣯⎦
As soon as the blk_jx
initialized, we can solve the power flow equations in block as
conv = ExaPF.nlsolve!(
NewtonRaphson(verbose=2),
blk_jx,
blk_stack;
)
Power flow has converged: true
* #iterations: 4
* Time Jacobian (s) ........: 0.0001
* Time linear solver (s) ...: 0.0000
* update (s) ............: 0.0000
* ldiv (s) ..............: 0.0000
* Time total (s) ...........: 0.0003
At the solution, we get different values for the voltage magnitudes at the PQ nodes:
reshape(blk_stack.vmag, nbus, nscen)
9×10 Matrix{Float64}:
1.0 1.0 1.0 1.0 … 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0 1.0 1.0
0.943541 0.95292 0.981164 0.950537 0.935226 0.93459 0.940441
0.914215 0.945201 0.969608 0.920305 0.904826 0.901628 0.912521
0.976902 0.972501 0.957086 0.983511 … 0.963949 0.946908 0.949376
0.97575 0.936577 0.931211 0.97245 0.927613 0.905909 0.955032
0.969623 0.966701 0.969266 0.975233 0.953494 0.945146 0.952849
0.933175 0.92961 0.952579 0.926164 0.939987 0.930848 0.91973
Solve power flow in batch on the GPU
When the BlockPolarForm
model is instantiated on the GPU, the expressions are evaluated in batch. The syntax to solve the power flow equations is exactly the same as on the CPU, using cusolverRF
to solve the different linear systems:
using CUDA, CUSOLVERRF
polar_gpu = ExaPF.load_polar("case9.m", CUDABackend());
blk_polar_gpu = ExaPF.BlockPolarForm(polar_gpu, nscen); # load model on GPU
blk_stack_gpu = ExaPF.NetworkStack(blk_polar_gpu);
ExaPF.set_params!(blk_stack_gpu, ploads, qloads);
powerflow_gpu = ExaPF.PowerFlowBalance(blk_polar_gpu) ∘ ExaPF.PolarBasis(blk_polar_gpu);
blk_jx_gpu = ExaPF.ArrowheadJacobian(blk_polar_gpu, powerflow_gpu, State());
ExaPF.set_params!(blk_jx_gpu, blk_stack_gpu);
ExaPF.jacobian!(blk_jx_gpu, blk_stack_gpu);
rf_fac = CUSOLVERRF.RFLU(blk_jx_gpu.J)
rf_solver = LS.DirectSolver(rf_fac)
conv = ExaPF.nlsolve!(
NewtonRaphson(verbose=2),
blk_jx_gpu,
blk_stack_gpu;
linear_solver=rf_solver,
)
Power flow has converged: true
* #iterations: 4
* Time Jacobian (s) ........: 0.0014
* Time linear solver (s) ...: 0.0514
* update (s) ............: 0.0266
* ldiv (s) ..............: 0.0249
* Time total (s) ...........: 0.0547