DC Optimal Power Flow

Similar to AC Optimal Power Flow, JuliaGrid utilizes the JuMP package to construct optimal power flow models, enabling users to manipulate these models using the standard functions provided by JuMP. JuliaGrid supports popular solvers mentioned in the JuMP documentation to solve the optimization problem.

To perform the DC optimal power flow, we first need to have the PowerSystem type that has been created with the DC model. After that, create the DcOptimalPowerFlow type to establish the DC optimal power flow framework using the function:


To solve the DC optimal power flow problem and acquire generator active power outputs and bus voltage angles, users can use of the following function:

After solving the DC optimal power flow, JuliaGrid provides function for computing powers:

Alternatively, instead of using functions responsible for solving optimal 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.


Optimal Power Flow Model

To set up the DC optimal power flow, we begin by creating the model. To illustrate this, consider the following:

using JuMP, Ipopt

system = powerSystem()

addBus!(system; label = "Bus 1", type = 3, angle = 0.17)
addBus!(system; label = "Bus 2", active = 0.1, conductance = 0.04)
addBus!(system; label = "Bus 3", active = 0.05)

@branch(minDiffAngle = -3.1, maxDiffAngle = 3.1, minFromBus = -0.12, maxFromBus = 0.12)
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)

@generator(minActive = 0.0)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 0.6, maxActive = 0.8)
addGenerator!(system; label = "Generator 2", bus = "Bus 2", active = 0.1, maxActive = 0.3)
addGenerator!(system; label = "Generator 3", bus = "Bus 2", active = 0.2, maxActive = 0.4)

cost!(system; generator = "Generator 1", active = 2, polynomial = [1100.2; 500; 80])
cost!(system; generator = "Generator 2", active = 1, piecewise = [8.0 11.0; 14.0 17.0])
cost!(system; generator = "Generator 3", active = 1, piecewise = [6 12.3; 8.7 16.8; 11 19])

dcModel!(system)

Next, the dcOptimalPowerFlow function is utilized to formulate the DC optimal power flow problem:

analysis = dcOptimalPowerFlow(system, Ipopt.Optimizer)
Info

All non-box two-sided constraints are modeled as intervals by default. However, users can choose to represent them as two separate constraints, one for the lower bound and one for the upper bound, by setting:

analysis = dcOptimalPowerFlow(system, Ipopt.Optimizer; interval = false)

Although this approach may be less efficient in terms of model creation and could lead to longer execution times depending on the solver, it allows for precise definition of the starting dual values.


Optimization Variables

In DC optimal power flow, generator active power outputs are linear functions of bus voltage angles. Thus, the model's variables include generator active power outputs and bus voltage angles:

julia> JuMP.all_variables(analysis.method.jump)7-element Vector{VariableRef}:
 angle[1]
 angle[2]
 angle[3]
 active[1]
 active[2]
 active[3]
 actwise[3]

It is important to highlight that when dealing with piecewise linear cost functions comprising multiple segments, as exemplified in the case of Generator 3, JuliaGrid automatically generates helper optimization variables, such as actwise[3], and formulates a set of linear constraints to appropriately handle these cost functions.

However, in instances where a piecewise linear cost function consists of only a single segment, as demonstrated by Generator 2, the function is modelled as a standard linear function, eliminating the necessity for additional helper optimization variables.

Please note that JuliaGrid keeps references to all variables categorized into three fields:

julia> fieldnames(typeof(analysis.method.variable.voltage))(:angle,)
julia> fieldnames(typeof(analysis.method.variable.power))(:active, :actwise)

Variable Names

Users have the option to define custom variable names for printing equations, which can help present them in a more compact form. For example:

analysis = dcOptimalPowerFlow(system, Ipopt.Optimizer; active = "P", angle = "θ")

Add Variables

Once the DcOptimalPowerFlow type is established, users can add new variables representing generator active power outputs by introducing additional generators. For example:

addGenerator!(analysis; label = "Generator 4", bus = "Bus 1", active = 0.1, maxActive = 0.2)

This command adds both a new variable and the corresponding box constraint to the optimization model.

To confirm that the variable has been successfully added, you can use the following function:

julia> JuMP.is_valid(analysis.method.jump, analysis.method.variable.power.active[4])true

Constraint Functions

JuliGrid keeps track of all the references to internally formed constraints in the constraint field of the DcOptimalPowerFlow type. These constraints are divided into six fields:

julia> fieldnames(typeof(analysis.method.constraint))(:slack, :capability, :balance, :voltage, :flow, :piecewise)

They fall into two main categories: box constraints and non-box constraints.

Info

We suggest that readers refer to the tutorial on DC Optimal Power Flow for insights into the implementation.


Box Constraints

The slack constraint is represented as an equality tied to the fixed voltage angle at the slack bus.

The capability constraints define variable bounds on active power generation and are always implemented as two separate constraints: one for the lower bound and one for the upper bound. If the bounds are equal or the generator is out-of-service, JuliaGrid models the constraint as an equality instead.


Non-Box Constraints

The balance constraints correspond to active power balance equations defined at each bus and are modeled as equality constraints.

The voltage constraints are associated with the minimum and maximum voltage angle difference between the from-bus and to-bus ends of each branch and are modeled as interval constraints by default. If the bounds are equal, an equality constraint is used instead.

The flow constraints, which refer to active power flow limits at the from-bus end of each branch, are also modeled as interval constraints by default. If the bounds are equal, an equality constraint is used.

If preferred, both voltage and flow constraints can be represented as two separate one-sided constraints, one for the lower and one for the upper bound, by setting the keyword argument interval = false when calling the dcOptimalPowerFlow function.

Finally, the piecewise constraints are introduced when piecewise linear cost functions with multiple segments are defined, and they impose only upper bounds.


Slack Bus Constraint

The slack field contains a reference to the equality constraint associated with the fixed bus voltage angle value of the slack bus. This constraint is set within the addBus! function using the angle keyword:

julia> print(system.bus.label, analysis.method.constraint.slack.angle)Bus 1: θ[1] = 0.17

Users have the flexibility to modify this constraint by changing which bus serves as the slack bus and by adjusting the value of the bus angle. This can be achieved using the updateBus! function, for example:

updateBus!(analysis; label = "Bus 1", angle = -0.1)

Subsequently, the updated slack constraint can be inspected as follows:

julia> print(system.bus.label, analysis.method.constraint.slack.angle)Bus 1: θ[1] = -0.1

Generator Active Power Capability Constraints

The capability field contains references to the box inequality constraints associated with the minimum and maximum active power outputs of the generators. These limits are specified using the minActive and maxActive keywords within the addGenerator! function:

julia> print(system.generator.label, analysis.method.constraint.capability.active)Generator 1: P[1] ≤ 0.8, P[1] ≥ 0
Generator 2: P[2] ≤ 0.3, P[2] ≥ 0
Generator 3: P[3] ≤ 0.4, P[3] ≥ 0
Generator 4: P[4] ≤ 0.2, P[4] ≥ 0

Let us now set Generator 2 out of service using the updateGenerator! function:

updateGenerator!(analysis; label = "Generator 2", status = 0)

We can now observe that the updated constraints reflect the current state of the system:

julia> print(system.generator.label, analysis.method.constraint.capability.active)Generator 1: P[1] ≤ 0.8, P[1] ≥ 0
Generator 2: P[2] = 0
Generator 3: P[3] ≤ 0.4, P[3] ≥ 0
Generator 4: P[4] ≤ 0.2, P[4] ≥ 0

Bus Active Power Balance Constraints

The balance field contains references to the equality constraints associated with the active power balance equations defined for each bus. The constant terms in these equations are determined by the active and conductance keywords within the addBus! function. Additionally, if there are phase shift transformers in the system, the constant terms can also be affected by the shiftAngle keyword within the addBranch! function:

julia> print(system.bus.label, analysis.method.constraint.balance.active)Bus 1: -120 θ[1] + 20 θ[2] + 100 θ[3] + P[1] + P[4] = 0
Bus 2: 20 θ[1] - 120 θ[2] + 100 θ[3] + P[3] = 0.14
Bus 3: 100 θ[1] + 100 θ[2] - 200 θ[3] = 0.05

During the execution of functions that add or update power system components, these constraints are automatically adjusted to reflect the current configuration of the power system, for example:

updateBus!(analysis; label = "Bus 3", active = 0.1)
updateGenerator!(analysis; label = "Generator 2", status = 1)

Subsequently, the updated set of active power balance constraints can be examined as follows:

julia> print(system.bus.label, analysis.method.constraint.balance.active)Bus 1: -120 θ[1] + 20 θ[2] + 100 θ[3] + P[1] + P[4] = 0
Bus 2: 20 θ[1] - 120 θ[2] + 100 θ[3] + P[2] + P[3] = 0.14
Bus 3: 100 θ[1] + 100 θ[2] - 200 θ[3] = 0.1

Bus Voltage Angle Difference Constraints

The voltage field contains references to the inequality constraints associated with the minimum and maximum bus voltage angle difference between the from-bus and to-bus ends of each branch. These values are specified using the minDiffAngle and maxDiffAngle keywords within the addBranch! function:

julia> print(system.branch.label, analysis.method.constraint.voltage.angle)Branch 1: θ[1] - θ[2] ∈ [-3.1, 3.1]
Branch 2: θ[1] - θ[3] ∈ [-3.1, 3.1]
Branch 3: θ[2] - θ[3] ∈ [-3.1, 3.1]
Info

If minDiffAngle = -2π and maxDiffAngle = 2π, or both are set to zero for a given branch, JuliaGrid will skip adding the corresponding inequality constraint.

Additionally, by employing the updateBranch! function, we have the ability to modify these constraints as follows:

updateBranch!(analysis; label = "Branch 1", minDiffAngle = -1.7, maxDiffAngle = 1.7)

Subsequently, the updated set of voltage angle difference constraints can be examined as follows:

julia> print(system.branch.label, analysis.method.constraint.voltage.angle)Branch 1: θ[1] - θ[2] ∈ [-1.7, 1.7]
Branch 2: θ[1] - θ[3] ∈ [-3.1, 3.1]
Branch 3: θ[2] - θ[3] ∈ [-3.1, 3.1]

Branch Active Power Flow Constraints

The flow field refers to the inequality constraints associated with active power flow limits at the from-bus end of each branch. These limits are set using the minFromBus and maxFromBus keywords in the addBranch! function:

julia> print(system.branch.label, analysis.method.constraint.flow.active)Branch 1: 20 θ[1] - 20 θ[2] ∈ [-0.12, 0.12]
Branch 2: 100 θ[1] - 100 θ[3] ∈ [-0.12, 0.12]
Branch 3: 100 θ[2] - 100 θ[3] ∈ [-0.12, 0.12]
Info

If the branch flow limits are set to minFromBus = 0.0 and maxFromBus = 0.0 for the corresponding branch, JuliGrid will omit the corresponding inequality constraint.

By employing the updateBranch! function, we have the ability to modify these specific constraints, for example:

updateBranch!(analysis; label = "Branch 1", status = 0)
updateBranch!(analysis; label = "Branch 2", reactance = 0.03, maxFromBus = 0.14)

Subsequently, the updated set of active power flow constraints can be examined as follows:

julia> print(system.branch.label, analysis.method.constraint.flow.active)Branch 2: 33.333333333333336 θ[1] - 33.333333333333336 θ[3] ∈ [-0.12, 0.14]
Branch 3: 100 θ[2] - 100 θ[3] ∈ [-0.12, 0.12]

Active Power Piecewise Constraints

In the context of active power modelling, the piecewise field serves as a reference to the inequality constraints related to linear piecewise cost functions. These constraints are created using the cost! function with active = 1 specified when dealing with piecewise linear cost functions comprising multiple segments. JuliaGrid takes care of establishing the appropriate inequality constraints for each segment of the piecewise linear cost:

julia> print(system.generator.label, analysis.method.constraint.piecewise.active)Generator 3: 1.6666666666666672 P[3] - actwise[3] ≤ -2.299999999999997
Generator 3: 0.9565217391304341 P[3] - actwise[3] ≤ -8.478260869565224

It is worth noting that these constraints can also be automatically updated using the cost! function, and readers can find more details in the section about the objective function.


Add Constraints

Users can easily introduce new constraints into the DC optimal power flow by using the addBranch! function. For example, to add a new branch to an existing PowerSystem and corresponding DcOptimalPowerFlow model:

addBranch!(analysis; label = "Branch 4", from = "Bus 1", to = "Bus 2", reactance = 1)

As a result, the flow constraints will be adjusted as follows:

julia> print(system.branch.label, analysis.method.constraint.flow.active)Branch 2: 33.333333333333336 θ[1] - 33.333333333333336 θ[3] ∈ [-0.12, 0.14]
Branch 3: 100 θ[2] - 100 θ[3] ∈ [-0.12, 0.12]
Branch 4: θ[1] - θ[2] ∈ [-0.12, 0.12]

Similarly, the addGenerator! function adds both a new variable and its associated box constraint.


Delete Constraints

When a branch or generator is taken out-of-service, JuliaGrid automatically adjusts the optimization problem to reflect that action, which may include removing certain constraints.

In some cases, it may also be useful to remove specific constraints manually. This can be done using the remove! function by specifying the constraint type: :slack, :capability, :balance, :voltage, :flow or :piecewise.

For example, to delete the constraint associated with the voltage angle difference at Branch 2, use:

remove!(analysis, :voltage; label = "Branch 2")

Alternatively, instead of using a label, constraints can also be deleted by index:

remove!(analysis, :voltage; index = 4)

After these operations, the remaining voltage angle difference constraints can be displayed as follows:

julia> print(system.branch.label, analysis.method.constraint.voltage.angle)Branch 3: θ[2] - θ[3] ∈ [-3.1, 3.1]
Info

In the event that a user deletes a constraint and subsequently executes a function that updates bus, branch, or generator parameters, and if the deleted constraint is affected by these functions, JuliaGrid will automatically reinstate that constraint.


Objective Function

The objective of the DC optimal power flow is constructed using polynomial and piecewise linear cost functions of the generators, which are defined using the cost! functions. Only polynomial cost functions of up to the second degree are included in the objective. Specifically, if a higher-degree polynomial is provided, JuliaGrid will discard all terms beyond the second degree and still include the resulting truncated polynomial in the objective function.

In the provided example, the objective function that needs to be minimized to obtain the optimal values of the active power outputs of the generators and the bus voltage angles is as follows:

julia> JuMP.objective_function(analysis.method.jump)1100.2 P[1]² + 500 P[1] + actwise[3] + P[2] + 83

Additionally, JuliaGrid stores the objective function in a separate variable, allowing users to access it by referencing the variable analysis.objective.


Update Objective Function

By utilizing the cost! functions, users have the flexibility to modify the objective function by adjusting polynomial or piecewise linear cost coefficients or by changing the type of polynomial or piecewise linear function employed. For instance, consider Generator 3, which incorporates a piecewise cost structure with two segments. Now, we can define a polynomial function for this generator and activate it by specifying the keyword active = 2 as shown:

cost!(analysis; generator = "Generator 3", active = 2, polynomial = [853.4; 257; 40])

This results in the updated objective function, which can be observed as follows:

julia> analysis.method.objective1100.2 P[1]² + 853.4 P[3]² + 500 P[1] + P[2] + 257 P[3] + 123

Setup Initial Values

In JuliaGrid, the assignment of initial primal and dual values for optimization variables takes place when the solve! function is executed.


Initial Primal Values

Initial primal values are determined based on the generator and voltage fields within the DcOptimalPowerFlow type. By default, these values are initially established using the active power outputs of the generators and the initial bus voltage angles:

julia> print(system.generator.label, analysis.power.generator.active)Generator 1: 0.6
Generator 2: 0.1
Generator 3: 0.2
Generator 4: 0.1
julia> print(system.bus.label, analysis.voltage.angle)Bus 1: -0.1 Bus 2: 0.0 Bus 3: 0.0

Users have the flexibility to adjust these values according to their specifications, which will then be used as the initial primal values when executing the solve! function.


Using DC Power Flow

In this perspective, users have the capability to conduct the DC power flow analysis and leverage the resulting solution to configure initial primal values. Here is an illustration of how this can be achieved:

flow = dcPowerFlow(system)
powerFlow!(flow; power = true)

After obtaining the solution, we can use the active power outputs of the generators, along with bus voltage angles, to set the initial values:

setInitialPoint!(analysis, flow)

Initial Dual Values

Dual variables, often referred to as Lagrange multipliers or Kuhn-Tucker multipliers, represent the shadow prices or marginal costs associated with constraints. The assignment of initial dual values occurs when the solve! function is executed. By default, dual values are undefined, but users can manually assign them using the addDual! function.

If a constraint is defined as an equality, an interval, or has only a lower or upper bound, it corresponds to a single dual variable. In such cases, an initial value can be set using the dual keyword. For example:

addDual!(analysis, :balance; label = "Bus 1", dual = 1e-3)

For constraints with both lower and upper bounds, users can assign initial dual values separately using the lower and upper keywords. For example:

addDual!(analysis, :capability; label = "Generator 1", lower = 500.0, upper = 0.0)

Alternatively, dual variables can be added by specifying the constraint index instead of a label:

addDual!(analysis, :capability; index = 1, lower = 500.0, upper = 0.0)

Optimal Power Flow Solution

To establish the DC optimal power flow problem, we utilize the dcOptimalPowerFlow function. After setting up the problem, we can use the solve! function to compute the optimal values for the active power outputs of the generators and the bus voltage angles:

solve!(analysis)

By executing this function, we will obtain the solution with the optimal values for the active power outputs of the generators and the bus voltage angles:

julia> print(system.generator.label, analysis.power.generator.active)Generator 1: -9.970032222936603e-9
Generator 2: 0.09540001454629071
Generator 3: -9.941368132382119e-9
Generator 4: 0.144600005365107
julia> print(system.bus.label, analysis.voltage.angle)Bus 1: -0.1 Bus 2: -0.1045999998228875 Bus 3: -0.10419999986716562

Objective Value

To obtain the objective value of the optimal power flow solution, we can use the objective_value function:

julia> JuMP.objective_value(analysis.method.jump)123.09539247459877

Dual Variables

The values of the dual variables are stored in the dual field of the DcOptimalPowerFlow type. For example:

julia> print(system.bus.label, analysis.method.dual.balance.active)Bus 1: 1.670899565213005e-7
Bus 2: 0.99999991627442
Bus 3: 1.0099998200006497

Wrapper Function

JuliaGrid provides a wrapper function for DC optimal power flow analysis and also supports the computation of powers using the powerFlow! function:

analysis = dcOptimalPowerFlow(system, Ipopt.Optimizer)
powerFlow!(analysis; verbose = 1)
EXIT: The optimal solution was found.

Users can utilize the functions printBusData and printGeneratorData to display results. Additionally, the functions listed in the Print Constraint Data section allow users to print constraint data related to buses, branches, or generators in the desired units. For example:

@power(MW, MVAr)
printBusConstraint(analysis)
|------------------------------|
| Bus Constraint Data          |
|------------------------------|
| Label | Active Power Balance |
|       |                      |
|       | Solution |      Dual |
|       |     [MW] | [$/MW-hr] |
|-------|----------|-----------|
| Bus 1 |   0.0000 |    0.0000 |
| Bus 2 |   0.0000 |    0.0100 |
| Bus 3 |  -0.0000 |    0.0101 |
|------------------------------|

Next, users can easily customize the print results for specific constraint, for example:

printBusConstraint(analysis; label = "Bus 1", header = true)
printBusConstraint(analysis; label = "Bus 2", 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
    printBusConstraint(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()
printBusConstraint(analysis, io; style = false)
CSV.write("constraint.csv", CSV.File(take!(io); delim = "|"))

Primal and Dual Warm Start

Utilizing the DcOptimalPowerFlow type and proceeding directly to the solver offers the advantage of a warm start. In this scenario, the initial primal and dual values for the subsequent solving step correspond to the solution obtained from the previous step.


Primal Variables

In the previous example, the following solution was obtained, representing the values of the primal variables:

julia> print(system.generator.label, analysis.power.generator.active)Generator 1: -9.909741590248875e-9
Generator 2: 0.09540004530353118
Generator 3: -9.818944909661503e-9
Generator 4: 0.14459997442515446
julia> print(system.bus.label, analysis.voltage.angle)Bus 1: -0.1 Bus 2: -0.10459999863520819 Bus 3: -0.10419999897640614

Dual Variables

We also obtained all dual values. Here, we list only the dual variables for one type of constraint as an example:

julia> print(system.branch.label, analysis.method.dual.flow.active)Branch 2: -1.03999892034497
Branch 3: 2.84066608018304e-7
Branch 4: -2.9169378638623428e-8

Modify Optimal Power Flow

Now, let us introduce changes to the power system from the previous example:

updateGenerator!(analysis; label = "Generator 2", maxActive = 0.08)

Next, we want to solve this modified optimal power flow problem. If we use solve! at this point, the primal and dual initial values will be set to the previously obtained values:

powerFlow!(analysis)

As a result, we obtain a new solution:

julia> print(system.generator.label, analysis.power.generator.active)Generator 1: -9.974942859089796e-9
Generator 2: 0.000800009969396404
Generator 3: 0.09459997966053971
Generator 4: 0.1446000203450072
julia> print(system.bus.label, analysis.voltage.angle)Bus 1: -0.1 Bus 2: -0.10460000039884865 Bus 3: -0.10420000029913648

Reset Primal and Dual Values

Users retain the flexibility to reset these initial primal values to their default configurations at any juncture. This can be accomplished by utilizing the active power outputs of the generators and the initial bus voltage angles extracted from the PowerSystem type, employing the setInitialPoint! function:

setInitialPoint!(analysis)

The primal initial values will now be identical to those that would be obtained if the dcOptimalPowerFlow function were executed after all the updates have been applied, while all dual variable values will be removed.


Extended Formulation

The JuMP model created by JuliaGrid is stored in the method.jump field of the DcOptimalPowerFlow type. This allows users to modify the model directly using JuMP macros and functions as needed. However, when making such modifications, users become responsible for tasks like setting initial values and extracting solutions, since these changes operate outside the standard JuliaGrid workflow.

Beyond this approach, JuliaGrid also provides a way to extend the standard DC optimal power flow formulation within its own framework. This lets users take advantage of features such as warm start and automatic solution storage, as described below.


Add Variable

User-defined variables can be added to the DC optimal power flow model using the @addVariable macro. It also allows immediate assignment of initial primal and dual values. For example:

@addVariable(analysis, 0.0 <= y <= 0.2, primal = 0.1, lower = 10.0, upper = 0.0)

We can also define collections of variables:

@addVariable(analysis, 0.0 <= x[i = 1:2] <= 0.4, primal = [0.1, 0.2], upper = [0.0; -2.5])

Add Constraints

Custom constraints can be added to the DC optimal power flow model using the @addConstraint macro. These constraints are not limited to user-defined variables; any optimization variable defined up to that point can be used. Let us focus on the voltage angle variables:

θ = analysis.method.variable.voltage.angle

Next, a new constraint can be defined, and at the same time, an initial dual value can be specified:

@addConstraint(analysis, 0.1 <= x[1] + 2 * x[2] + y + θ[2] <= 0.2, dual = 0.0)

Collections of constraints can also be defined:

@addConstraint(analysis, [i = 1:2], x[i] + 2 * θ[i] <= 0.6, dual = [0.0; 0.5])

Delete Constraints

To remove a constraint, use the remove! function with the :constraint symbol. For example, to remove the first added constraint:

remove!(analysis, :constraint; index = 1)

Objective Function

Users can modify the objective function using the set_objective_function function from the JuMP package. Here is an example of how it can be achieved:

expr = 1100.2 * analysis.method.variable.power.active[1]^2 - 50 * x[1] - x[2]^2 + y + 123
JuMP.set_objective_function(analysis.method.jump, analysis.method.objective - expr)

We can now observe the updated objective function as follows:

julia> JuMP.objective_function(analysis.method.jump)853.4 active[3]² + x[2]² + 500 active[1] + 257 active[3] + active[2] + 50 x[1] - y

Optimal Power Flow Solution

Users can now solve the extended formulation using:

powerFlow!(analysis; verbose = 1)
EXIT: The optimal solution was found.

After solving, users can access the optimal values as follows:

julia> analysis.power.generator.active4-element Vector{Float64}:
 -9.970011350911707e-9
  0.0008000099640822899
  0.09459997967174905
  0.14460002033418
julia> analysis.voltage.angle3-element Vector{Float64}: -0.1 -0.10460000039862187 -0.1042000002989664
julia> analysis.extended.solution3-element Vector{Float64}: 0.19999999500567658 -9.700113509543211e-9 0.00017117776009241908

Power Analysis

After obtaining the solution from the DC optimal power flow, we can calculate powers related to buses and branches using the power! function. For instance, let us consider the power system for which we obtained the DC optimal power flow solution:

using Ipopt

system = powerSystem()

addBus!(system; label = "Bus 1", type = 3, angle = 0.17)
addBus!(system; label = "Bus 2", active = 0.1, conductance = 0.04)
addBus!(system; label = "Bus 3", active = 0.05)

@branch(minDiffAngle = -pi, maxDiffAngle = pi, minFromBus = -0.12, maxFromBus = 0.12)
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)

@generator(minActive = 0.0)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 3.2, maxActive = 0.5)
addGenerator!(system; label = "Generator 2", bus = "Bus 2", active = 0.2, maxActive = 0.2)

cost!(system; generator = "Generator 1", active = 2, polynomial = [1100.2; 500; 80])
cost!(system; generator = "Generator 2", active = 1, piecewise = [10.8 12.3; 14.7 16.8])

analysis = dcOptimalPowerFlow(system, Ipopt.Optimizer)
powerFlow!(analysis)

Now we can calculate the active powers using the following function:

power!(analysis)

Finally, to display the active power injections and from-bus active power flows, we can use the following code:

julia> print(system.bus.label, analysis.power.injection.active)Bus 1: -9.628433161879002e-9
Bus 2: 0.09000000962843388
Bus 3: -0.04999999999999716
julia> print(system.branch.label, analysis.power.from.active)Branch 1: -0.007142859893837783 Branch 2: 0.007142850265404177 Branch 3: 0.04285714973459309
Info

To better understand the powers associated with buses and branches that are calculated by the power! function, we suggest referring to the tutorials on DC Optimal Power Flow.


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 data with the desired units, users can use the following function:

@voltage(pu, deg)
@power(MW, MVAr)
printBusData(analysis)
|---------------------------------------------------------------------|
| Bus Data                                                            |
|---------------------------------------------------------------------|
| Label | Voltage | Power Generation | Power Demand | Power Injection |
|       |         |                  |              |                 |
|   Bus |   Angle |           Active |       Active |          Active |
|       |   [deg] |             [MW] |         [MW] |            [MW] |
|-------|---------|------------------|--------------|-----------------|
| Bus 1 |  9.7403 |          -0.0000 |       0.0000 |         -0.0000 |
| Bus 2 |  9.7607 |          19.0000 |      10.0000 |          9.0000 |
| Bus 3 |  9.7362 |           0.0000 |       5.0000 |         -5.0000 |
|---------------------------------------------------------------------|

Active Power Injection

To calculate active power injection associated with a specific bus, the function can be used:

julia> active = injectionPower(analysis; label = "Bus 2")0.09000000962843388

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 2")0.19000000962843167

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.007142850265404177
julia> active = toPower(analysis; label = "Branch 2")-0.007142850265404177