DC Power Flow
To perform the DC power flow, we first need to have the PowerSystem
type that has been created with the DC model. Following that, we can construct the power flow model encapsulated within the DcPowerFlow
type by employing the following function:
To solve the DC power flow problem and acquire bus voltage angles, users can use the following function:
Once the DC power flow solution is obtained, JuliaGrid provides a function for computing powers:
Alternatively, instead of using functions responsible for solving power flow and computing powers, users can use the wrapper function:
Users can also access specialized functions for computing specific types of powers for individual buses, branches, or generators within the power system.
Power Flow Solution
To solve the DC power flow problem using JuliaGrid, we start by creating the PowerSystem
type and defining the DC model with the dcModel!
function. Here is an example:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", type = 1, active = 0.1)
addBus!(system; label = "Bus 3", type = 1, active = 0.05)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.05)
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 3", reactance = 0.01)
addBranch!(system; label = "Branch 3", from = "Bus 2", to = "Bus 3", reactance = 0.01)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 3.2)
dcModel!(system)
The dcPowerFlow
function can be used to establish the DC power flow problem:
analysis = dcPowerFlow(system)
By default, the user activates LU
factorization to solve the system of linear equations. Users may also choose the LDLt
, QR
or KLU
factorization methods explicitly:
analysis = dcPowerFlow(system, KLU)
The KLU
method, using the Gilbert-Peierls algorithm, can significantly speed up power flow computations [1].
To obtain the bus voltage angles, we can call the solve!
function as follows:
solve!(analysis)
Once the solution is obtained, the bus voltage angles can be accessed using:
julia> print(system.bus.label, analysis.voltage.angle)
Bus 1: 0.0 Bus 2: -0.0017857142857142859 Bus 3: -0.001142857142857143
For implementation insights, we suggest referring to the tutorial on DC Power Flow Analysis.
Wrapper Function
JuliaGrid provides a wrapper function for DC power flow analysis and also supports the computation of powers using the powerFlow! function:
analysis = dcPowerFlow(system)
powerFlow!(analysis; verbose = 2)
Number of entries in the nodal matrix: 9
Number of state variables: 2
EXIT: The solution of the DC power flow was found.
Print Results in the REPL
Users have the option to print the results in the REPL using any units that have been configured, such as:
@voltage(pu, deg)
printBusData(analysis)
|-----------------|
| Bus Data |
|-----------------|
| Label | Voltage |
| | |
| Bus | Angle |
| | [deg] |
|-------|---------|
| Bus 1 | 0.0000 |
| Bus 2 | -0.1023 |
| Bus 3 | -0.0655 |
|-----------------|
Next, users can easily customize the print results for specific buses, for example:
printBusData(analysis; label = "Bus 1", header = true)
printBusData(analysis; label = "Bus 2")
printBusData(analysis; label = "Bus 3", footer = true)
Save Results to a File
Users can also redirect print output to a file. For example, data can be saved in a text file as follows:
open("bus.txt", "w") do file
printBusData(analysis, file)
end
Save Results to a CSV File
For CSV output, users should first generate a simple table with style = false
, and then save it to a CSV file:
using CSV
io = IOBuffer()
printBusData(analysis, io; style = false)
CSV.write("bus.csv", CSV.File(take!(io); delim = "|"))
Power System Update
We begin by creating the PowerSystem
type with the powerSystem
function. The DC model is then configured using dcModel!
function. After that, we initialize the DcPowerFlow
type through the dcPowerFlow
function and solve the resulting power flow problem:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", type = 2, active = 2.1)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.2)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 3.2)
dcModel!(system)
analysis = dcPowerFlow(system)
powerFlow!(analysis)
Next, we modify the existing PowerSystem
type within the DC model using add and update functions. Then, we create a new DcPowerFlow
type based on the modified system and solve the power flow problem:
updateBus!(system; label = "Bus 2", active = 0.4)
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 0.3)
updateBranch!(system; label = "Branch 1", status = 0)
addGenerator!(system; label = "Generator 2", bus = "Bus 2", active = 1.5)
updateGenerator!(system; label = "Generator 1", active = 1.9)
analysis = dcPowerFlow(system)
powerFlow!(analysis)
This concept removes the need to restart and recreate the PowerSystem
within the dc
field from the beginning when implementing changes to the existing power system.
Power Flow Update
An advanced methodology involves users establishing the DcPowerFlow
type just once. After this initial setup, users can integrate new branches and generators, and also have the capability to modify buses, branches, and generators, all without the need to recreate the DcPowerFlow
type. This is particularly beneficial when the previously computed nodal matrix factorization can be reused.
Let us now revisit our defined PowerSystem
and DcPowerFlow
types:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", type = 2, active = 2.1)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.2)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 3.2)
dcModel!(system)
analysis = dcPowerFlow(system)
powerFlow!(analysis)
Next, we modify the existing PowerSystem
within the DC model as well as the DcPowerFlow
type using add and update functions. We then immediately proceed to solve the power flow problem:
updateBus!(analysis; label = "Bus 2", active = 0.4)
addBranch!(analysis; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 0.3)
updateBranch!(analysis; label = "Branch 1", status = 0)
addGenerator!(analysis; label = "Generator 2", bus = "Bus 2", active = 1.5)
updateGenerator!(analysis; label = "Generator 1", active = 1.9)
powerFlow!(analysis)
This concept removes the need to restart and recreate both the PowerSystem
within the dc
field and the DcPowerFlow
from the beginning when implementing changes to the existing power system. Additionally, JuliaGrid can reuse symbolic factorizations of LU or LDLt, as long as the nonzero pattern of the nodal matrix remains consistent between power system configurations.
Reusing Matrix Factorization
Drawing from the preceding example, our focus now shifts to finding a solution involving modifications that entail adjusting the active power demand at Bus 2
, introducing a new generator at Bus 2
, and fine-tuning the output power of Generator 1
. It is important to note that these adjustments do not impact the branches, leaving the nodal matrix unchanged. To resolve this updated system, users can simply execute the powerFlow!
function:
updateBus!(analysis; label = "Bus 2", active = 0.2)
addGenerator!(analysis; label = "Generator 3", bus = "Bus 2", active = 0.3)
updateGenerator!(analysis; label = "Generator 1", active = 2.1)
powerFlow!(analysis)
In this scenario, JuliaGrid will recognize instances where the user has not modified branch parameters affecting the nodal matrix. Consequently, JuliaGrid will leverage the previously performed nodal matrix factorization, resulting in a significantly faster solution compared to recomputing the factorization.
Limitations
Attempting to change the slack bus or leaving the existing slack bus without a connected generator, and then proceeding directly to the power flow calculation, is not feasible. In such cases, JuliaGrid will raise an error:
julia> updateGenerator!(analysis; label = "Generator 1", status = 0)
ERROR: KeyError: key 1 not found
To resolve this, the user must recreate the DcPowerFlow
type rather than attempting to reuse the existing one:
updateGenerator!(system; label = "Generator 1", status = 0)
analysis = dcPowerFlow(system)
powerFlow!(analysis)
Power Analysis
After obtaining the solution, we can calculate powers related to buses, branches, and generators using the power!
function. For example, let us consider the power system for which we obtained the DC power flow solution:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", type = 1, active = 0.1)
addBus!(system; label = "Bus 3", type = 1, active = 0.05)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.05)
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 3", reactance = 0.01)
addBranch!(system; label = "Branch 3", from = "Bus 2", to = "Bus 3", reactance = 0.01)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 3.2)
analysis = dcPowerFlow(system)
powerFlow!(analysis)
Now we can calculate the active powers using the following function:
power!(analysis)
Next, let us convert the base power unit to megavolt-amperes (MVA):
@base(system, MVA, V)
Finally, here are the calculated active power values in megawatts (MW) corresponding to buses and branches:
julia> print(system.bus.label, system.base.power.value * analysis.power.injection.active)
Bus 1: 15.000000000000002 Bus 2: -10.0 Bus 3: -5.0
julia> print(system.branch.label, system.base.power.value * analysis.power.from.active)
Branch 1: 3.571428571428572 Branch 2: 11.428571428571429 Branch 3: -6.42857142857143
To better understand the powers associated with buses, branches, and generators that are calculated by the power!
function, we suggest referring to the tutorials on DC Power Flow Analysis.
Print Results in the REPL
Users can utilize any of the print functions outlined in the Print Power System Data or Print Power System Summary. For example, users have the option to print the results in the REPL using any units that have been configured:
@power(MW, pu)
printBranchData(analysis)
|-----------------------------------------------------------------------|
| Branch Data |
|-----------------------------------------------------------------------|
| Label | From-Bus Power | To-Bus Power | Status |
| | | | |
| Branch | From-Bus | To-Bus | Active | Active | |
| | | | [MW] | [MW] | |
|----------|----------|--------|----------------|--------------|--------|
| Branch 1 | Bus 1 | Bus 1 | 3.5714 | -3.5714 | 1 |
| Branch 2 | Bus 1 | Bus 1 | 11.4286 | -11.4286 | 1 |
| Branch 3 | Bus 2 | Bus 2 | -6.4286 | 6.4286 | 1 |
|-----------------------------------------------------------------------|
Active Power Injection
To calculate active power injection associated with a specific bus, the function can be used:
julia> active = injectionPower(analysis; label = "Bus 1")
0.15000000000000002
Active Power Injection from Generators
To calculate active power injection from the generators at a specific bus, the function can be used:
julia> active = supplyPower(analysis; label = "Bus 1")
0.15000000000000002
Active Power Flow
Similarly, we can compute the active power flow at both the from-bus and to-bus ends of the specific branch by utilizing the provided functions below:
julia> active = fromPower(analysis; label = "Branch 2")
0.1142857142857143
julia> active = toPower(analysis; label = "Branch 2")
-0.1142857142857143
Generator Active Power Output
Finally, we can compute the active power output of a particular generator using the function:
julia> active = generatorPower(analysis; label = "Generator 1")
0.15000000000000002