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)
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.
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]
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]
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]
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.objective
1100.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.
Print Results in the REPL
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.active
4-element Vector{Float64}: -9.970011350911707e-9 0.0008000099640822899 0.09459997967174905 0.14460002033418
julia> analysis.voltage.angle
3-element Vector{Float64}: -0.1 -0.10460000039862187 -0.1042000002989664
julia> analysis.extended.solution
3-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
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.
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 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