AC Power Flow
To perform the AC power flow analysis, we will first need the PowerSystem
type that has been created with the AC model. Following that, we can construct the power flow model encapsulated within the AcPowerFlow
type by employing one of the following functions:
To obtain bus voltages and solve the power flow problem, users can implement an iterative process using functions:
After solving the AC power flow, JuliaGrid provides functions for computing powers and currents:
Alternatively, instead of designing their own iteration process and computing powers and currents, users can use the wrapper function:
Users can also access specialized functions for computing specific types of powers and currents for individual buses, branches, or generators within the power system.
Finally, the package provides two functions for reactive power limit validation of generators and adjusting the voltage angles to match an arbitrary bus angle:
Bus Type Modification
Depending on how the system is constructed, the types of buses that are initially set are checked and can be changed during the construction of the AcPowerFlow
type.
To explain the details, we consider a power system and assume that the Newton-Raphson method has been chosen:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", type = 2)
addBus!(system; label = "Bus 3", type = 2)
addGenerator!(system; bus = "Bus 2")
analysis = newtonRaphson(system)
Initially, Bus 1
is set as the slack bus (type = 3
), and Bus 2
and Bus 3
are generator buses (type = 2
). However, Bus 3
does not have a generator, and JuliaGrid considers this a mistake and changes the corresponding bus to a demand bus (type = 1
).
After this step, JuliaGrid verifies the slack bus. Initially, the slack bus (type = 3
) corresponds to Bus 1
, but since it does not have an in-service generator connected to it, JuliaGrid recognizes it as another mistake. Therefore, JuliaGrid assigns a new slack bus from the available generator buses (type = 2
) that have connected in-service generators. In this specific example, Bus 2
becomes the new slack bus.
As a result, we can observe the updated array of bus types:
julia> print(system.bus.label, system.bus.layout.type)
Bus 1: 1 Bus 2: 3 Bus 3: 1
Note that, if a bus is initially defined as the demand bus (type = 1
) and later a generator is added to it, the bus type will not be changed to the generator bus (type = 2
). Instead, it will remain as a demand bus.
Only the type of these buses that are defined as generator buses (type = 2
) but do not have a connected in-service generator will be changed to demand buses (type = 1
).
The bus that is defined as the slack bus (type = 3
) but lacks a connected in-service generator will have its type changed to the demand bus (type = 1
). Meanwhile, the first generator bus (type = 2
) with an in-service generator connected to it will be assigned as the new slack bus (type = 3
).
Setup Initial Voltages
Let us create the PowerSystem
type and select the Newton-Raphson method:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, magnitude = 1.0, angle = 0.0)
addBus!(system; label = "Bus 2", type = 1, magnitude = 0.9, angle = -0.1)
addBus!(system; label = "Bus 3", type = 2, magnitude = 0.8, angle = -0.2)
addGenerator!(system; bus = "Bus 1", magnitude = 1.3)
addGenerator!(system; bus = "Bus 2", magnitude = 1.1)
addGenerator!(system; bus = "Bus 3", magnitude = 1.2)
acModel!(system)
analysis = newtonRaphson(system)
Here, the function newtonRaphson
initializes voltages in polar coordinates.
The initial voltage magnitudes are set to:
julia> print(system.bus.label, analysis.voltage.magnitude)
Bus 1: 1.3 Bus 2: 0.9 Bus 3: 1.2
This vector is created based on the bus types by selecting voltage magnitude values from the PowerSystem
type, using the vectors:
julia> [system.bus.voltage.magnitude system.generator.voltage.magnitude]
3×2 Matrix{Float64}: 1.0 1.3 0.9 1.1 0.8 1.2
The initial voltage angles are set to:
julia> print(system.bus.label, analysis.voltage.angle)
Bus 1: 0.0 Bus 2: -0.1 Bus 3: -0.2
This vector is derived from the voltage angle values in the PowerSystem
type:
julia> system.bus.voltage.angle
3-element Vector{Float64}: 0.0 -0.1 -0.2
The rule governing the specification of initial voltage magnitudes is simple. If a bus has an in-service generator and is declared the generator bus (type = 2
), then the initial voltage magnitudes are specified using the setpoint provided within the generator. This is because the generator bus has known values of voltage magnitude that are specified within the generator.
On the other hand, the slack bus (type = 3
) always requires an in-service generator. The initial value of the voltage magnitude at the slack bus is determined exclusively by the setpoints provided within the generators connected to it. This is a result of the slack bus having a known voltage magnitude that must be maintained.
If there are multiple generators connected to the generator or slack bus, the initial voltage magnitude will align with the magnitude setpoint specified for the first in-service generator in the list.
Custom Initial Voltages
This method of specifying initial values has a significant advantage in that it allows the user to easily change the initial voltage magnitudes and angles, which play a crucial role in iterative methods. For instance, suppose we define our power system as follows:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, magnitude = 1.0, angle = 0.0)
addBus!(system; label = "Bus 2", type = 1, magnitude = 0.9, angle = -0.1)
addBus!(system; label = "Bus 3", type = 2, magnitude = 0.8, angle = -0.2)
addGenerator!(system; bus = "Bus 1", magnitude = 1.1)
addGenerator!(system; bus = "Bus 3", magnitude = 1.2)
acModel!(system)
Now, the user can initiate a "flat start", this can be easily done as follows:
for i = 1:system.bus.number
system.bus.voltage.magnitude[i] = 1.0
system.bus.voltage.angle[i] = 0.0
end
analysis = newtonRaphson(system)
The initial voltage values are:
julia> print(system.bus.label, analysis.voltage.magnitude, analysis.voltage.angle)
Bus 1: 1.1, 0.0 Bus 2: 1.0, 0.0 Bus 3: 1.2, 0.0
Consequently, the iteration begins with a fixed set of voltage magnitude values that remain constant throughout the iteration process. The remaining values are initialized as part of the "flat start" approach.
Power Flow Solution
To start, we will create a power system and define the AC model by invoking the acModel!
function:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.5, magnitude = 0.9, angle = 0.0)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.05, magnitude = 1.1, angle = -0.1)
addBus!(system; label = "Bus 3", type = 1, active = 0.5, magnitude = 1.0, angle = -0.2)
@branch(resistance = 0.02, conductance = 1e-4, susceptance = 0.04)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.05)
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 0.01)
addBranch!(system; label = "Branch 3", from = "Bus 2", to = "Bus 3", reactance = 0.04)
@generator(active = 3.2)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", magnitude = 1.1)
addGenerator!(system; label = "Generator 2", bus = "Bus 2", magnitude = 1.2)
acModel!(system)
Once the AC model is defined, we can choose the method to solve the power flow problem. JuliaGrid provides four methods: newtonRaphson
, fastNewtonRaphsonBX
, fastNewtonRaphsonXB
, and gaussSeidel
.
For example, to use the Newton-Raphson method to solve the power flow problem, we can use:
analysis = newtonRaphson(system)
By default, the user activates LU factorization to solve the system of linear equations within each iteration of the Newton-Raphson method. However, users can specifically opt for the QR
factorization method:
analysis = newtonRaphson(system, QR)
The capability to change the factorization method is exclusively available for the Newton-Raphson and fast Newton-Raphson methods.
This function sets up the desired method for an iterative process based on two functions: mismatch!
and solve!
. The mismatch!
function calculates the active and reactive power injection mismatches using the given voltage magnitudes and angles, while solve!
computes the voltage magnitudes and angles.
To perform an iterative process with the Newton-Raphson or fast Newton-Raphson methods in JuliaGrid, the mismatch!
function must be included inside the iteration loop. For instance:
for iteration = 1:100
mismatch!(analysis)
solve!(analysis)
end
Upon completion of the AC power flow analysis, the solution is conveyed through the bus voltage magnitudes and angles. Here are the values corresponding to the buses:
julia> print(system.bus.label, analysis.voltage.magnitude, analysis.voltage.angle)
Bus 1: 1.1, 0.0 Bus 2: 1.1312984943545519, 0.022034582385283743 Bus 3: 1.123144964293639, 0.005894335154140862
In contrast, the iterative loop of the Gauss-Seidel method does not require the mismatch!
function:
analysis = gaussSeidel(system)
for iteration = 1:100
solve!(analysis)
end
In these examples, the algorithms run until the specified number of iterations is reached.
We recommend that the reader refer to the tutorial on AC Power Flow Analysis, where we explain the implementation of the methods and algorithm structures in detail.
Breaking the Iterative Process
We can terminate the iterative process using the mismatch!
function. The following code shows an example of how to use the function to break out of the iteration loop:
analysis = newtonRaphson(system)
for iteration = 1:100
stopping = mismatch!(analysis)
if all(stopping .< 1e-8)
println("Solution found in $(analysis.method.iteration) iterations.")
break
end
solve!(analysis)
end
Solution found in 4 iterations.
The mismatch!
function returns the maximum absolute values of active and reactive power injection mismatches, which are commonly used as a convergence criterion in iterative AC power flow algorithms.
Wrapper Function
JuliaGrid provides a wrapper function powerFlow!
for AC power flow analysis. Hence, it offers a way to solve AC power flow with reduced implementation effort:
analysis = newtonRaphson(system)
powerFlow!(analysis; verbose = 3)
Number of buses: 3 Number of shunts: 0 Number of generators: 2
Demand: 2 Capacitor: 0 In-service: 2
Generator: 0 Reactor: 0 Out-of-service: 0
Number of branches: 3 Number of lines: 3 Number of transformers: 0
In-service: 3 In-service: 3 In-service: 0
Out-of-service: 0 Out-of-service: 0 Out-of-service: 0
Number of entries in the Jacobian: 16
Number of state variables: 4
---------------------------------------------------------------
Iteration Maximum Active Mismatch Maximum Reactive Mismatch
---------------------------------------------------------------
0 4.06374168e+00 7.07929344e+00
1 6.81049807e-01 3.09577339e-01
2 2.78429581e-02 1.47017435e-02
3 4.20637696e-05 2.41301211e-05
4 9.17410037e-11 5.41143311e-11
Minimum Value Maximum Value
Magnitude Increment: 2.1783e-07 9.6134e-07
Angle Increment: 5.7916e-07 1.7668e-06
EXIT: The solution was found using the Newton-Raphson method in 4 iterations.
Users can choose any of the approaches presented in this section to solve AC power flow based on their needs. Additionally, users can review the algorithm used in the wrapper function within the AC Power Flow tutorial section. For example, they can refer to the Newton-Raphson algorithm.
Combining Methods
The PowerSystem
type, once created, can be shared among different methods, offering several advantages.
For instance, while the Gauss-Seidel method is commonly used to swiftly derive an approximate solution, the Newton-Raphson method is favored for obtaining precise final solutions. Hence, a strategy involves employing the Gauss-Seidel method for a limited number of iterations, followed by initializing the Newton-Raphson method with the voltages obtained from the Gauss-Seidel method, leveraging it as a starting point for further refinement:
gs = gaussSeidel(system)
for iteration = 1:5
solve!(gs)
end
Next, we can initialize the Newton-Raphson method with the voltages obtained from the Gauss-Seidel method and start the algorithm from that point:
analysis = newtonRaphson(system)
setInitialPoint!(analysis, gs)
powerFlow!(analysis; verbose = 1)
EXIT: The solution was found using the Newton-Raphson method in 2 iterations.
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 | Magnitude | Angle |
| | [pu] | [deg] |
|-------|-----------|--------|
| Bus 1 | 1.1000 | 0.0000 |
| Bus 2 | 1.1313 | 1.2625 |
| Bus 3 | 1.1231 | 0.3377 |
|----------------------------|
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
After establishing the PowerSystem
type using the powerSystem
function and configuring the AC model with acModel!
, users gain the capability to incorporate new branches and generators. Furthermore, they can adjust buses, branches, and generators.
Once updates are done, users can progress towards generating the AcPowerFlow
type using the newtonRaphson
, fastNewtonRaphsonBX
, fastNewtonRaphsonXB
, or gaussSeidel
function. Ultimately, resolving the AC power flow is achieved through the utilization of the mismatch!
and solve!
functions:
system = powerSystem() # <- Initialize the PowerSystem instance
addBus!(system; label = "Bus 1", type = 3, active = 0.5, magnitude = 0.9, angle = 0.0)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.05, magnitude = 1.1, angle = -0.1)
@branch(resistance = 0.02, conductance = 1e-4, susceptance = 0.04)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.05)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", magnitude = 1.1, active = 3.2)
acModel!(system)
analysis = newtonRaphson(system) # <- Build AcPowerFlow for the defined power system
powerFlow!(analysis)
updateBus!(system; label = "Bus 2", active = 0.2)
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 1)
updateBranch!(system; label = "Branch 1", status = 0)
addGenerator!(system; label = "Generator 2", bus = "Bus 1", active = 0.2)
updateGenerator!(system; label = "Generator 1", active = 0.3)
analysis = newtonRaphson(system) # <- Build AcPowerFlow for the updated power system
powerFlow!(analysis)
This concept removes the need to restart and recreate the PowerSystem
within the ac
field from the beginning when implementing changes to the existing power system.
Power Flow Update
An advanced methodology involves users establishing the AcPowerFlow
type using newtonRaphson
, fastNewtonRaphsonBX
, fastNewtonRaphsonXB
, or gaussSeidel
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 AcPowerFlow
type.
This advancement extends beyond the previous scenario where recreating the PowerSystem
and AC model was unnecessary, to now include the scenario where AcPowerFlow
also does not need to be recreated. Such efficiency proves particularly beneficial in cases where JuliaGrid can reuse established Jacobian matrices or even factorizations, especially when users choose the fast Newton-Raphson method.
By modifying the previous example, we observe that we now create the AcPowerFlow
type only once using the newtonRaphson
function. This approach allows us to circumvent the need for reinitializing the Jacobian matrix, enabling us to proceed directly with iterations:
system = powerSystem() # <- Initialize the PowerSystem instance
addBus!(system; label = "Bus 1", type = 3, active = 0.5, magnitude = 0.9, angle = 0.0)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.05, magnitude = 1.1, angle = -0.1)
@branch(resistance = 0.02, conductance = 1e-4, susceptance = 0.04)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.05)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", magnitude = 1.1, active = 3.2)
acModel!(system)
analysis = newtonRaphson(system) # <- Build AcPowerFlow for the defined power system
powerFlow!(analysis)
updateBus!(analysis; label = "Bus 2", active = 0.2)
addBranch!(analysis; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 1)
updateBranch!(analysis; label = "Branch 1", status = 0)
addGenerator!(analysis; label = "Generator 2", bus = "Bus 1", active = 0.2)
updateGenerator!(analysis; label = "Generator 1", active = 0.3)
# <- No need for re-build; we have already updated the existing AcPowerFlow instance
powerFlow!(analysis)
This concept removes the need to restart and recreate both the PowerSystem
within the ac
field and the AcPowerFlow
from the beginning when implementing changes to the existing power system.
Fast Newton-Raphson: Reusing Jacobian Matrices Factorizations
An intriguing scenario unfolds when employing the fast Newton-Raphson method. Continuing from the previous example, let us now initialize the fast Newton-Raphson method and proceed with iterations as outlined below:
analysis = fastNewtonRaphsonBX(system)
powerFlow!(analysis; verbose = 1)
EXIT: The solution was found using the fast Newton-Raphson method in 6 iterations.
Now, let us make changes to the power system and proceed directly to the iteration step:
updateBus!(analysis; label = "Bus 2", reactive = 0.04)
updateGenerator!(analysis; label = "Generator 1", reactive = 0.1)
powerFlow!(analysis; verbose = 1)
EXIT: The solution was found using the fast Newton-Raphson method in 6 iterations.
In this scenario, JuliaGrid identifies cases where the user has not altered parameters that impact the Jacobian matrices. Consequently, JuliaGrid efficiently utilizes the previously performed factorizations, leading to a notably faster solution compared to recomputing the factorization process.
Warm Start
In these scenarios, users leverage the previously created PowerSystem
type with the AC model and also reuse the AcPowerFlow
type, proceeding directly to the iterations. This approach offers the advantage of a "warm start", wherein the initial voltages for the subsequent iteration step align with the solution from the previous iteration step. This alignment facilitates an efficient continuation of the power flow analysis.
Let us now make another alteration to the power system:
updateBus!(analysis; label = "Bus 1", active = 0.1, magnitude = 0.95, angle = -0.07)
updateGenerator!(analysis; label = "Generator 2", reactive = 0.2, magnitude = 1.1)
With these modifications we are not only altering the power system, but also initial voltages. For the next uses of one of the methods, these values now are:
julia> print(system.bus.label, analysis.voltage.magnitude, analysis.voltage.angle)
Bus 1: 1.1, -0.07 Bus 2: 1.0637733424443538, -0.17150926526233395
Therefore, users possess the flexibility to adjust these initial values as needed by employing the magnitude
and angle
keywords within the updateBus!
and updateGenerator!
functions.
If users prefer to set initial voltages according to the typical scenario, they can accomplish this through the setInitialPoint!
function:
setInitialPoint!(analysis)
Now, we have initial voltages defined exclusively according to the PowerSystem
. These values are exactly the same as if we executed the newtonRaphson
, fastNewtonRaphsonBX
, fastNewtonRaphsonXB
, or gaussSeidel
function after all the updates we performed:
julia> print(system.bus.label, analysis.voltage.magnitude, analysis.voltage.angle)
Bus 1: 1.1, -0.07 Bus 2: 1.1, -0.1
Limitations
The newtonRaphson
, fastNewtonRaphsonBX
, fastNewtonRaphsonXB
, or gaussSeidel
function oversees bus type validations, as outlined in the Bus Type Modification section. Consequently, attempting to change bus types or leaving generator buses without a generator and then proceeding directly to the iteration process is not viable.
In such scenarios, JuliaGrid will raise an error:
julia> updateBus!(analysis; label = "Bus 2", type = 2)
ERROR: The power flow model cannot be reused because the bus type configuration has changed.
In this scenario, the user must execute the newtonRaphson
, fastNewtonRaphsonBX
, fastNewtonRaphsonXB
, or gaussSeidel
function instead of trying to reuse them, for example:
updateBus!(system; label = "Bus 2", type = 2)
analysis = fastNewtonRaphsonBX(system)
powerFlow!(analysis)
After creating the PowerSystem
and AcPowerFlow
types, users can add or modify buses, branches, and generators before directly proceeding to iterations. JuliaGrid automatically executes the necessary functions when adjustments lead to a valid solution. However, if modifications are incompatible, like altering bus types, JuliaGrid raises an error to prevent misleading outcomes, ensuring accuracy.
Power and Current Analysis
After obtaining the solution from the AC power flow, we can calculate various electrical quantities related to buses, branches, and generators using the power!
and current!
functions. For instance, let us consider the power system for which we obtained the AC power flow solution:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.6)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.1, susceptance = 0.03)
addBus!(system; label = "Bus 3", type = 1, conductance = 0.02)
@branch(resistance = 0.02, conductance = 1e-4, susceptance = 0.04)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.5)
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 0.1)
addBranch!(system; label = "Branch 3", from = "Bus 2", to = "Bus 3", reactance = 0.4)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 0.2)
addGenerator!(system; label = "Generator 2", bus = "Bus 2", active = 1.0, reactive = 0.2)
analysis = newtonRaphson(system)
powerFlow!(analysis; verbose = 1)
EXIT: The solution was found using the Newton-Raphson method in 3 iterations.
We can now utilize the provided functions to compute powers and currents:
power!(analysis)
current!(analysis)
For instance, if we want to show the active power injections and the to-bus current angles, we can employ the following code:
julia> print(system.bus.label, analysis.power.injection.active)
Bus 1: -0.9645033643807599 Bus 2: 1.0000000022772795 Bus 3: -4.359533802980213e-10
julia> print(system.branch.label, analysis.current.to.angle)
Branch 1: -0.15017765586673948 Branch 2: -0.08939059735692804 Branch 3: -3.073480808526896
For a better understanding of the powers and currents from buses, branches, and generators obtained by the power!
and current!
functions, refer to the AC 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, to create a bus summary with the desired units, users can use the following function:
@voltage(pu, deg)
@power(MW, MVAr)
printBusSummary(analysis)
|----------------------------------------------------------------------------|
| Bus Summary |
|----------------------------------------------------------------------------|
| Type | Minimum | Maximum | In-Use | Total |
| | | | | |
| | Label | Value | Label | Value | | |
|-------------------|-------|----------|-------|----------|--------|---------|
| Voltage | | | | | 3 | |
| Magnitude [pu] | Bus 1 | 1.0000 | Bus 3 | 1.0362 | | |
| Angle [deg] | Bus 1 | 0.0000 | Bus 2 | 4.3886 | | |
|-------------------|-------|----------|-------|----------|--------|---------|
| Power Generation | | | | | 2 | |
| Active [MW] | Bus 1 | -36.4503 | Bus 2 | 100.0000 | | 63.5497 |
| Reactive [MVAr] | Bus 1 | -17.6931 | Bus 2 | 20.0000 | | 2.3069 |
|-------------------|-------|----------|-------|----------|--------|---------|
| Power Demand | | | | | 2 | |
| Active [MW] | Bus 2 | 0.0000 | Bus 1 | 60.0000 | | 60.0000 |
| Reactive [MVAr] | Bus 1 | 0.0000 | Bus 2 | 10.0000 | | 10.0000 |
|-------------------|-------|----------|-------|----------|--------|---------|
| Power Injection | | | | | 3 | |
| Active [MW] | Bus 1 | -96.4503 | Bus 2 | 100.0000 | | 3.5497 |
| Reactive [MVAr] | Bus 1 | -17.6931 | Bus 2 | 10.0000 | | -7.6931 |
|-------------------|-------|----------|-------|----------|--------|---------|
| Shunt Power | | | | | 2 | |
| Active [MW] | Bus 2 | 0.0000 | Bus 3 | 2.1475 | | 2.1475 |
| Reactive [MVAr] | Bus 2 | -3.1727 | Bus 3 | -0.0000 | | -3.1727 |
|-------------------|-------|----------|-------|----------|--------|---------|
| Current Injection | | | | | 3 | |
| Magnitude [pu] | Bus 3 | 0.0000 | Bus 1 | 0.9806 | | |
| Angle [rad] | Bus 3 | -2.7799 | Bus 1 | 2.9602 | | |
|----------------------------------------------------------------------------|
Active and Reactive Power Injection
To calculate the active and reactive power injection associated with a specific bus, the function can be used:
julia> active, reactive = injectionPower(analysis; label = "Bus 1")
(-0.9645033643807599, -0.17693071258892878)
Active and Reactive Power Injection from Generators
To calculate the active and reactive power injection from the generators at a specific bus, the function can be used:
julia> active, reactive = supplyPower(analysis; label = "Bus 1")
(-0.3645033643807599, -0.17693071258892878)
Active and Reactive Power at Shunt Element
To calculate the active and reactive power associated with shunt element at a specific bus, the function can be used:
julia> active, reactive = shuntPower(analysis; label = "Bus 3")
(0.021474952575336232, -0.0)
Active and Reactive Power Flow
Similarly, we can compute the active and reactive power flow at both the from-bus and to-bus ends of the specific branch by utilizing the functions provided below:
julia> active, reactive = fromPower(analysis; label = "Branch 2")
(-0.8053928587014492, -0.11256618205670144)
julia> active, reactive = toPower(analysis; label = "Branch 2")
(0.8186418709823436, 0.13714551291797428)
Active and Reactive Power at Charging Admittances
To calculate the active and reactive power linked with branch charging admittances of the particular branch, the function can be used:
julia> active, reactive = chargingPower(analysis; label = "Branch 1")
(0.00010287834702025066, -0.04115133880810026)
Active powers indicate active losses within the branch's charging admittances. Moreover, charging admittances injected reactive powers into the power system due to their capacitive nature.
Active and Reactive Power at Series Impedance
To calculate the active and reactive power across the series impedance of the branch, the function can be used:
julia> active, reactive = seriesPower(analysis; label = "Branch 2")
(0.013146133933874347, 0.06573066966937174)
The active power also considers active losses originating from the series resistance of the branch, while the reactive power represents reactive losses resulting from the impedance's inductive characteristics.
Generator Active and Reactive Power Output
We can compute the active and reactive power output of a particular generator using the function:
julia> active, reactive = generatorPower(analysis; label = "Generator 1")
(-0.3645033643807599, -0.17693071258892878)
Current Injection
To calculate the current injection associated with a specific bus, the function can be used:
julia> magnitude, angle = injectionCurrent(analysis; label = "Bus 1")
(0.9805973776015471, 2.9601674608926722)
Current Flow
We can compute the current flow at both the from-bus and to-bus ends of the specific branch by utilizing the provided functions below:
julia> magnitude, angle = fromCurrent(analysis; label = "Branch 2")
(0.8132212504540907, 3.0027266550776193)
julia> magnitude, angle = toCurrent(analysis; label = "Branch 2")
(0.807142930926126, -0.08939059735692804)
Current Through Series Impedance
To calculate the current passing through the series impedance of the branch in the direction from the from-bus end to the to-bus end, we can use the following function:
julia> magnitude, angle = seriesCurrent(analysis; label = "Branch 2")
(0.8107445323242811, 3.027168837917323)
Generator Reactive Power Limits
The function reactiveLimit!
can be used to check if the generators' output of reactive power is within the defined limits after obtaining the solution from the AC power flow analysis:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", type = 1, active = 0.5)
addBus!(system; label = "Bus 3", type = 2, reactive = 0.05)
addBus!(system; label = "Bus 4", type = 2, reactive = 0.05)
@branch(resistance = 0.015)
addBranch!(system; from = "Bus 1", to = "Bus 2", reactance = 0.05)
addBranch!(system; from = "Bus 1", to = "Bus 3", reactance = 0.01)
addBranch!(system; from = "Bus 2", to = "Bus 3", reactance = 0.04)
addBranch!(system; from = "Bus 2", to = "Bus 4", reactance = 0.004)
@generator(minReactive = -0.4, maxReactive = 0.1)
addGenerator!(system; label = "Generator 1", bus = "Bus 1")
addGenerator!(system; label = "Generator 2", bus = "Bus 3", reactive = 0.8)
addGenerator!(system; label = "Generator 3", bus = "Bus 4", reactive = 0.9)
analysis = newtonRaphson(system)
powerFlow!(analysis)
violate = reactiveLimit!(analysis)
The output reactive power of the observed generators is subject to limits which are defined as follows:
julia> [system.generator.capability.minReactive system.generator.capability.maxReactive]
3×2 Matrix{Float64}: -0.4 0.1 -0.4 0.1 -0.4 0.1
After obtaining the solution of the AC power flow analysis, the reactiveLimit!
function is used to internally calculate the output powers of the generators and verify if these values exceed the defined limits. Consequently, the variable violate
indicates whether there is a violation of limits.
In the provided example, it can be observed that the Generator 2
and Generator 3
violate the maximum limit:
julia> print(system.generator.label, violate)
Generator 1: 0 Generator 2: 1 Generator 3: 1
Due to these violations of limits, the PowerSystem
type undergoes modifications, and the output reactive power at the limit-violating generators is adjusted as follows:
julia> print(system.generator.label, system.generator.output.reactive)
Generator 1: 0.0 Generator 2: 0.1 Generator 3: 0.1
To ensure that these values stay within the limits, the bus type must be changed from the generator bus (type = 2
) to the demand bus (type = 1
), as shown below:
julia> print(system.bus.label, system.bus.layout.type)
Bus 1: 3 Bus 2: 1 Bus 3: 1 Bus 4: 1
After modifying the PowerSystem
type as described earlier, we can run the simulation again with the following code:
analysis = newtonRaphson(system)
powerFlow!(analysis)
Once the simulation is complete, we can verify that all generator reactive power outputs now satisfy the limits by checking the violate variable again:
julia> violate = reactiveLimit!(analysis)
3-element Vector{Int64}: 0 0 0
The reactiveLimit!
function changes the PowerSystem
type deliberately because it is intended to help users create the power system where all reactive power outputs of the generators are within limits.
New Slack Bus
Looking at the following code example, we can see that the output limits of the generator are set only for Generator 1
that is connected to the slack bus:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.5, reactive = 0.05)
addBus!(system; label = "Bus 2", type = 1, active = 0.5)
addBus!(system; label = "Bus 3", type = 2)
addBus!(system; label = "Bus 4", type = 2)
@branch(resistance = 0.01)
addBranch!(system; from = "Bus 1", to = "Bus 2", reactance = 0.05)
addBranch!(system; from = "Bus 1", to = "Bus 3", reactance = 0.01)
addBranch!(system; from = "Bus 2", to = "Bus 3", reactance = 0.04)
addBranch!(system; from = "Bus 2", to = "Bus 4", reactance = 0.004)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", maxReactive = 0.2)
addGenerator!(system; label = "Generator 2", bus = "Bus 4", reactive = 0.3)
analysis = newtonRaphson(system)
powerFlow!(analysis)
Upon checking the limits, we can observe that the slack bus has been transformed by executing the following code:
julia> violate = reactiveLimit!(analysis)
[ Info: The slack bus labeled Bus 1 is converted to generator bus. The bus labeled Bus 4 is the new slack bus. 2-element Vector{Int64}: -1 0
Here, the generator connected to the slack bus is violating the minimum reactive power limit, which indicates the need to convert the slack bus. It is important to note that the new slack bus can be created only from the generator bus (type = 2
). We will now perform another AC power flow analysis on the modified system using the following:
analysis = newtonRaphson(system)
powerFlow!(analysis)
After examining the bus voltages, we will focus on the angles:
julia> print(system.bus.label, analysis.voltage.angle)
Bus 1: 0.013581064245677444 Bus 2: 0.0005651921742472857 Bus 3: 0.01069570666159161 Bus 4: 0.0
We can observe that the angles have been calculated based on the new slack bus. JuliaGrid offers the function to adjust these angles to match the original slack bus as follows:
adjustAngle!(analysis; slack = "Bus 1")
After executing the above code, the updated results can be viewed:
julia> print(system.bus.label, analysis.voltage.angle)
Bus 1: 0.0 Bus 2: -0.013015872071430158 Bus 3: -0.0028853575840858334 Bus 4: -0.013581064245677444