AC Optimal Power Flow
This example performs multiple AC optimal power flow analyses on the power system shown in Figure 1. These simulations capture quasi-steady-state conditions under varying constraints and topology changes.
Figure 1: The 4-bus power system.
Users can download a Julia script containing the scenarios from this section using the following link.
We begin by defining the units for active and reactive powers, as well as voltage magnitudes and angles, which will be used throughout this example:
@power(MW, MVAr)
@voltage(pu, deg)
Next, we define bus parameters for the AC optimal power flow analysis. This includes specifying the slack bus (type = 3
), where the bus voltage angle is set to zero by default, along with the corresponding active
and reactive
power loads, and shunt capacitor banks with conductance
and susceptance
values. The voltage magnitude limits for each bus are set using minMagnitude
and maxMagnitude
. With these definitions, we can begin building the power system model:
system = powerSystem()
@bus(minMagnitude = 0.95, maxMagnitude = 1.05)
addBus!(system; label = "Bus 1", type = 3)
addBus!(system; label = "Bus 2", active = 20.2, reactive = 10.5)
addBus!(system; label = "Bus 3", conductance = 0.1, susceptance = 8.2)
addBus!(system; label = "Bus 4", active = 50.8, reactive = 23.1)
We define the transmission line parameters by specifying resistance
, reactance
, and susceptance
values. At this stage, we do not impose any branch flow constraints, but they will be introduced later in the example:
@branch(label = "Branch ?", reactance = 0.22)
addBranch!(system; from = "Bus 1", to = "Bus 3", resistance = 0.02, susceptance = 0.05)
addBranch!(system; from = "Bus 1", to = "Bus 2", resistance = 0.05, susceptance = 0.04)
addBranch!(system; from = "Bus 2", to = "Bus 3", resistance = 0.04, susceptance = 0.04)
addBranch!(system; from = "Bus 3", to = "Bus 4", turnsRatio = 0.98)
We define the active
and reactive
power outputs of the generators, which serve as initial primal values for the optimization variables related to generator outputs. Reactive power outputs of the generators are limited by minReactive
and maxReactive
, while active power outputs vary between minActive
and maxActive
:
@generator(label = "Generator ?", minActive = 2.0, minReactive = -15.5, maxReactive = 15.5)
addGenerator!(system; bus = "Bus 1", active = 63.1, reactive = 8.2, maxActive = 65.5)
addGenerator!(system; bus = "Bus 2", active = 3.0, reactive = 6.2, maxActive = 20.5)
addGenerator!(system; bus = "Bus 2", active = 4.1, reactive = 8.5, maxActive = 22.4)
Finally, we define the active power supply costs of the generators in polynomial form by setting active = 2
. Then, we express the polynomial as a quadratic using the polynomial
keyword:
cost!(system; generator = "Generator 1", active = 2, polynomial = [0.04; 20.0; 0.0])
cost!(system; generator = "Generator 2", active = 2, polynomial = [1.00; 20.0; 0.0])
cost!(system; generator = "Generator 3", active = 2, polynomial = [1.00; 20.0; 0.0])
After defining the power system data, we generate an AC model that includes essential vectors and matrices for analysis, such as the nodal admittance matrix:
acModel!(system)
Display Data Settings
Before running simulations, we configure the data display settings, including the selection of displayed data elements and the numeric format for relevant power values.
For bus-related data, we set:
show1 = Dict("Power Injection" => false)
fmt1 = Dict("Power Generation" => "%.2f", "Power Demand" => "%.2f", "Shunt Power" => "%.2f")
Similarly, for branch-related data, we choose:
show2 = Dict("Shunt Power" => false, "Status" => false)
fmt2 = Dict("From-Bus Power" => "%.2f", "To-Bus Power" => "%.2f", "Series Power" => "%.2f")
To display generator-related data, we also set:
show3 = Dict("Reactive Power Capability" => false)
fmt3 = Dict("Power Output" => "%.2f")
Base Case Analysis
First, we create the AC optimal power flow model and select the Ipopt
solver. Next, we solve the model to determine bus voltage magnitudes and angles, along with the active and reactive power outputs of the generators. Afterward, we compute the remaining power values for buses and branches:
analysis = acOptimalPowerFlow(system, Ipopt.Optimizer)
powerFlow!(analysis, power = true, verbose = 1)
EXIT: The optimal solution was found.
Once the AC optimal power flow is solved, we can review the bus-related results, including the optimal values of bus voltage magnitudes and angles:
printBusData(analysis; show = show1, fmt = fmt1)
|------------------------------------------------------------------------------------------|
| Bus Data |
|------------------------------------------------------------------------------------------|
| Label | Voltage | Power Generation | Power Demand | Shunt Power |
| | | | | |
| Bus | Magnitude | Angle | Active | Reactive | Active | Reactive | Active | Reactive |
| | [pu] | [deg] | [MW] | [MVAr] | [MW] | [MVAr] | [MW] | [MVAr] |
|-------|-----------|----------|--------|----------|--------|----------|--------|----------|
| Bus 1 | 1.0500 | 0.0000 | 65.50 | 7.96 | 0.00 | 0.00 | 0.00 | -0.00 |
| Bus 2 | 1.0391 | -3.0998 | 6.31 | 15.52 | 20.20 | 10.50 | 0.00 | -0.00 |
| Bus 3 | 1.0182 | -4.4294 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | -8.50 |
| Bus 4 | 0.9809 | -10.7249 | 0.00 | 0.00 | 50.80 | 23.10 | 0.00 | -0.00 |
|------------------------------------------------------------------------------------------|
The optimal active and reactive outputs of the generators are as follows:
printGeneratorData(analysis; fmt = fmt3)
|--------------------------------------------------|
| Generator Data |
|--------------------------------------------------|
| Label | Power Output | Status |
| | | |
| Generator | Bus | Active | Reactive | |
| | | [MW] | [MVAr] | |
|-------------|-------|--------|----------|--------|
| Generator 1 | Bus 1 | 65.50 | 7.96 | 1 |
| Generator 2 | Bus 2 | 3.16 | 7.74 | 1 |
| Generator 3 | Bus 2 | 3.16 | 7.79 | 1 |
|--------------------------------------------------|
As we can observe from the generator data, the Generator 1
, which has the lowest costs, generates power at the maximum value. Additionally, we can observe that Generator 2
and Generator 3
have the same cost functions, which dictate that these two will produce an equal amount of active power.
We enabled users to display bus, branch, or generator data related to the optimal power analysis. For instance, for generator data, we can observe that the dual variables related to Generator 1
are different from zero, indicating that the generator's output has reached its limit:
printGeneratorConstraint(analysis; show = show3)
|--------------------------------------------------------|
| Generator Constraint Data |
|--------------------------------------------------------|
| Label | Active Power Capability |
| | |
| | Minimum | Solution | Maximum | Dual |
| | [MW] | [MW] | [MW] | [$/MW-hr] |
|-------------|---------|----------|---------|-----------|
| Generator 1 | 2.0000 | 65.5000 | 65.5000 | -0.5987 |
| Generator 2 | 2.0000 | 3.1563 | 20.5000 | 0.0000 |
| Generator 3 | 2.0000 | 3.1563 | 22.4000 | 0.0000 |
|--------------------------------------------------------|
Finally, we can also review the results related to branches:
printBranchData(analysis; show = show2, fmt = fmt2)
|------------------------------------------------------------------------------------------|
| Branch Data |
|------------------------------------------------------------------------------------------|
| Label | From-Bus Power | To-Bus Power | Series Power |
| | | | |
| Branch | From-Bus | To-Bus | Active | Reactive | Active | Reactive | Active | Reactive |
| | | | [MW] | [MVAr] | [MW] | [MVAr] | [MW] | [MVAr] |
|----------|----------|--------|--------|----------|--------|----------|--------|----------|
| Branch 1 | Bus 1 | Bus 3 | 38.72 | 10.34 | -38.42 | -12.36 | 0.30 | 3.33 |
| Branch 2 | Bus 1 | Bus 2 | 26.78 | -2.39 | -26.45 | -0.55 | 0.33 | 1.43 |
| Branch 3 | Bus 2 | Bus 3 | 12.57 | 5.57 | -12.48 | -9.36 | 0.08 | 0.44 |
| Branch 4 | Bus 3 | Bus 4 | 50.80 | 30.22 | -50.80 | -23.10 | -0.00 | 7.12 |
|------------------------------------------------------------------------------------------|
Thus, we obtained the active and reactive power flows, as illustrated in Figure 2.
(a) Active powers.
(b) Reactive powers.
Figure 2: Power flows in the 4-bus power system for the base case scenario.
Modifying Demands
Let us now introduce a new state by updating the active and reactive power demands of consumers. These updates modify both the power system model and the AC optimal power flow model simultaneously:
updateBus!(analysis; label = "Bus 2", active = 25.2, reactive = 13.5)
updateBus!(analysis; label = "Bus 4", active = 43.3, reactive = 18.6)
Next, we solve the AC optimal power flow again to compute the new solution without recreating the model. This step enables a warm start, as the initial primal and dual values correspond to those obtained in the base case:
powerFlow!(analysis, power = true, verbose = 1)
EXIT: The optimal solution was found.
Now, we observe the power output of the generators:
printGeneratorData(analysis; fmt = fmt3)
|--------------------------------------------------|
| Generator Data |
|--------------------------------------------------|
| Label | Power Output | Status |
| | | |
| Generator | Bus | Active | Reactive | |
| | | [MW] | [MVAr] | |
|-------------|-------|--------|----------|--------|
| Generator 1 | Bus 1 | 63.65 | 3.37 | 1 |
| Generator 2 | Bus 2 | 2.79 | 7.66 | 1 |
| Generator 3 | Bus 2 | 2.79 | 7.66 | 1 |
|--------------------------------------------------|
Compared to the base case, all generators have reduced power supplies due to lower demand. It is important to note that, although one might expect Generator 1
to continue producing at maximum output because of its lower cost, while only Generator 2
and Generator 3
reduce their production, this is not the case. The reason is that the optimal power flow must also satisfy power balance and bus voltage magnitude constraints.
At the end of this scenario, we can review branch-related results for a more comprehensive insight into power flows:
printBranchData(analysis; show = show2, fmt = fmt2)
|------------------------------------------------------------------------------------------|
| Branch Data |
|------------------------------------------------------------------------------------------|
| Label | From-Bus Power | To-Bus Power | Series Power |
| | | | |
| Branch | From-Bus | To-Bus | Active | Reactive | Active | Reactive | Active | Reactive |
| | | | [MW] | [MVAr] | [MW] | [MVAr] | [MW] | [MVAr] |
|----------|----------|--------|--------|----------|--------|----------|--------|----------|
| Branch 1 | Bus 1 | Bus 3 | 35.73 | 5.99 | -35.49 | -8.69 | 0.25 | 2.70 |
| Branch 2 | Bus 1 | Bus 2 | 27.91 | -2.62 | -27.56 | -0.19 | 0.35 | 1.56 |
| Branch 3 | Bus 2 | Bus 3 | 7.95 | 2.00 | -7.92 | -6.11 | 0.03 | 0.16 |
| Branch 4 | Bus 3 | Bus 4 | 43.30 | 23.45 | -43.30 | -18.60 | 0.00 | 4.85 |
|------------------------------------------------------------------------------------------|
The obtained results allow us to illustrate the active and reactive power flows in Figure 3.
(a) Active powers.
(b) Reactive powers.
Figure 3: Power flows in the 4-bus power system with modified demands.
Modifying Generator Costs
We modify the cost functions for all generators, altering the objective function of the AC optimal power flow. By modifying the cost function of Generator 1
, we shift it from being the lowest-cost to the highest-cost generator in the system. Updating both the power system model and the AC optimal power flow model simultaneously allows us to enable a warm start for the optimization problem:
cost!(analysis; generator = "Generator 1", active = 2, polynomial = [2.0; 20.0; 0.0])
cost!(analysis; generator = "Generator 2", active = 2, polynomial = [0.8; 20.0; 0.0])
cost!(analysis; generator = "Generator 3", active = 2, polynomial = [0.8; 20.0; 0.0])
Next, we solve the updated problem and calculate the resulting powers:
powerFlow!(analysis, power = true, verbose = 1)
EXIT: The optimal solution was found.
The optimal active and reactive power outputs of the generators are as follows:
printGeneratorData(analysis; fmt = fmt3)
|--------------------------------------------------|
| Generator Data |
|--------------------------------------------------|
| Label | Power Output | Status |
| | | |
| Generator | Bus | Active | Reactive | |
| | | [MW] | [MVAr] | |
|-------------|-------|--------|----------|--------|
| Generator 1 | Bus 1 | 25.98 | 1.87 | 1 |
| Generator 2 | Bus 2 | 20.50 | 7.11 | 1 |
| Generator 3 | Bus 2 | 22.40 | 7.11 | 1 |
|--------------------------------------------------|
In this scenario, we observe that, due to the increased cost of Generator 1
, both Generator 2
and Generator 3
have increased their production to the maximum possible values to capitalize on their lower costs. The remaining required active power is then supplied by Generator 1
.
We can also review the results related to branches for this scenario:
printBranchData(analysis; show = show2, fmt = fmt2)
|------------------------------------------------------------------------------------------|
| Branch Data |
|------------------------------------------------------------------------------------------|
| Label | From-Bus Power | To-Bus Power | Series Power |
| | | | |
| Branch | From-Bus | To-Bus | Active | Reactive | Active | Reactive | Active | Reactive |
| | | | [MW] | [MVAr] | [MW] | [MVAr] | [MW] | [MVAr] |
|----------|----------|--------|--------|----------|--------|----------|--------|----------|
| Branch 1 | Bus 1 | Bus 3 | 23.14 | 4.30 | -23.03 | -8.55 | 0.11 | 1.17 |
| Branch 2 | Bus 1 | Bus 2 | 2.84 | -2.43 | -2.83 | -1.96 | 0.00 | 0.02 |
| Branch 3 | Bus 2 | Bus 3 | 20.53 | 2.69 | -20.37 | -6.13 | 0.16 | 0.89 |
| Branch 4 | Bus 3 | Bus 4 | 43.30 | 23.41 | -43.30 | -18.60 | 0.00 | 4.81 |
|------------------------------------------------------------------------------------------|
Figure 4 illustrates the power flows for this scenario. Compared to the previous scenario, Figure 4a shows that Branch 2
has significantly lower active power flow, while Branch 3
has become considerably more loaded.
(a) Active powers.
(b) Reactive powers.
Figure 4: Power flows in the 4-bus power system with modified generator costs.
Adding Branch Flow Constraints
To limit active power flow, we introduce constraints on Branch 2
and Branch 3
by setting type = 1
, where the active power flow at the from-bus end of these branches is limited using the maxFromBus
keyword:
updateBranch!(analysis; label = "Branch 2", type = 1, maxFromBus = 15.0)
updateBranch!(analysis; label = "Branch 3", type = 1, maxFromBus = 15.0)
Next, we recalculate the AC optimal power flow:
powerFlow!(analysis, power = true, verbose = 1)
EXIT: The optimal solution was found.
Now, let us observe the generator outputs:
printGeneratorData(analysis; fmt = fmt3)
|--------------------------------------------------|
| Generator Data |
|--------------------------------------------------|
| Label | Power Output | Status |
| | | |
| Generator | Bus | Active | Reactive | |
| | | [MW] | [MVAr] | |
|-------------|-------|--------|----------|--------|
| Generator 1 | Bus 1 | 42.10 | -13.52 | 1 |
| Generator 2 | Bus 2 | 13.46 | 15.50 | 1 |
| Generator 3 | Bus 2 | 13.46 | 15.50 | 1 |
|--------------------------------------------------|
The power flow limit at Branch 3
forces Generator 1
to increase its active power output despite its higher cost compared to Generator 2
and Generator 3
, due to the need to satisfy all constraints. Additionally, we also observe a significant redistribution in the production of reactive powers.
We can review the branch data constraints and observe that the active power at the from-bus end of Branch 3
reaches the defined limit, which leads to the power redistribution described earlier, while the power flow at Branch 2
stays within the specified limits:
printBranchConstraint(analysis)
|-----------------------------------------------------|
| Branch Constraint Data |
|-----------------------------------------------------|
| Label | From-Bus Active Power Flow |
| | |
| | Minimum | Solution | Maximum | Dual |
| | [MW] | [MW] | [MW] | [$/MW-hr] |
|----------|---------|----------|---------|-----------|
| Branch 2 | 0.0000 | 13.4196 | 15.0000 | -0.0000 |
| Branch 3 | 0.0000 | 15.0000 | 15.0000 | -439.7438 |
|-----------------------------------------------------|
Finally, we can review the branch-related data to examine the redistribution of powers in detail:
printBranchData(analysis; show = show2, fmt = fmt2)
|------------------------------------------------------------------------------------------|
| Branch Data |
|------------------------------------------------------------------------------------------|
| Label | From-Bus Power | To-Bus Power | Series Power |
| | | | |
| Branch | From-Bus | To-Bus | Active | Reactive | Active | Reactive | Active | Reactive |
| | | | [MW] | [MVAr] | [MW] | [MVAr] | [MW] | [MVAr] |
|----------|----------|--------|--------|----------|--------|----------|--------|----------|
| Branch 1 | Bus 1 | Bus 3 | 28.68 | -0.26 | -28.52 | -3.31 | 0.16 | 1.71 |
| Branch 2 | Bus 1 | Bus 2 | 13.42 | -13.25 | -13.28 | 9.55 | 0.14 | 0.63 |
| Branch 3 | Bus 2 | Bus 3 | 15.00 | 7.95 | -14.88 | -11.60 | 0.12 | 0.65 |
| Branch 4 | Bus 3 | Bus 4 | 43.30 | 23.50 | -43.30 | -18.60 | 0.00 | 4.90 |
|------------------------------------------------------------------------------------------|
Based on the obtained results, we can illustrate the power flows in Figure 5.
(a) Active powers.
(b) Reactive powers.
Figure 5: Power flows in the 4-bus power system with added branch flow constraints.
Modifying Network Topology
At the end, we set Branch 2
out-of-service:
updateBranch!(analysis; label = "Branch 2", status = 0)
We then recalculate the AC optimal power flow:
powerFlow!(analysis; power = true, verbose = 1)
EXIT: The optimal solution was found.
We can now observe the updated generator outputs:
printGeneratorData(analysis; fmt = fmt3)
|--------------------------------------------------|
| Generator Data |
|--------------------------------------------------|
| Label | Power Output | Status |
| | | |
| Generator | Bus | Active | Reactive | |
| | | [MW] | [MVAr] | |
|-------------|-------|--------|----------|--------|
| Generator 1 | Bus 1 | 28.66 | 4.88 | 1 |
| Generator 2 | Bus 2 | 20.10 | 7.94 | 1 |
| Generator 3 | Bus 2 | 20.10 | 7.94 | 1 |
|--------------------------------------------------|
Due to the outage of Branch 2
and the flow limit at Branch 3
, Generator 1
faces difficulties supplying load at Bus 2
, reducing its output. Consequently, the only solution is to increase the output of Generator 2
and Generator 3
.
Upon reviewing the branch data, we observe that the active power flows in the remaining in-service branches remain largely unchanged. This is because, following the outage of Branch 2
, Generator 2
and Generator 3
have taken over the responsibility of supplying the load at Bus 2
, effectively displacing Generator 1
:
printBranchData(analysis; show = show2, fmt = fmt2)
|------------------------------------------------------------------------------------------|
| Branch Data |
|------------------------------------------------------------------------------------------|
| Label | From-Bus Power | To-Bus Power | Series Power |
| | | | |
| Branch | From-Bus | To-Bus | Active | Reactive | Active | Reactive | Active | Reactive |
| | | | [MW] | [MVAr] | [MW] | [MVAr] | [MW] | [MVAr] |
|----------|----------|--------|--------|----------|--------|----------|--------|----------|
| Branch 1 | Bus 1 | Bus 3 | 28.66 | 4.88 | -28.50 | -8.54 | 0.16 | 1.76 |
| Branch 2 | Bus 1 | Bus 2 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
| Branch 3 | Bus 2 | Bus 3 | 15.00 | 2.37 | -14.91 | -6.18 | 0.09 | 0.50 |
| Branch 4 | Bus 3 | Bus 4 | 43.30 | 23.42 | -43.30 | -18.60 | 0.00 | 4.82 |
|------------------------------------------------------------------------------------------|
Figure 6 illustrates these results under the outage of Branch 2
.
(a) Active powers.
(b) Reactive powers.
Figure 6: Power flows in the 4-bus power system with modified network topology.