| file_format | mystnb | ||
|---|---|---|---|
| kernelspec |
|
Alternating current optimal power flow (ACOPF) is a fundamental nonlinear optimization problem in power systems. It is used to determine the optimal power flow of a power system to minimize the generation cost while satisfying the power flow equations and system constraints.
In this example, we will show how to use PyOptInterface to solve an single-period optimal power flow problem using the structured nonlinear programming formulation.
The decision variables are the active power output of the generators
The objective function is the total cost of the generators, which is the sum of the quadratic cost, the linear cost, and the constant cost. The first constraint ensures that the phase angle of the reference bus is zero. The second and third constraints are the active and reactive power output limits of the generators. The fourth constraint is the voltage magnitude limits of the buses. The fifth and sixth constraints are the power balance equations of the buses. The seventh and eighth constraints are the power flow equations of the branches. The ninth and tenth constraints are the power flow equations of the branches in the opposite direction. The eleventh and twelfth constraints are the apparent power limits of the branches. The last constraint is the phase angle difference limits of the branches.
We will use PJM 5-bus system as an example to demonstrate the implementation of the optimal power flow problem. The PJM 5-bus system is a small power system with 5 buses and 6 branches. The system data is shown below.
branches = [
# (from, to, R, X, B, angmin, angmax, Smax)
(0, 1, 0.00281, 0.0281, 0.00712, -30.0, 30.0, 4.00),
(0, 3, 0.00304, 0.0304, 0.00658, -30.0, 30.0, 4.26),
(0, 4, 0.00064, 0.0064, 0.03126, -30.0, 30.0, 4.26),
(1, 2, 0.00108, 0.0108, 0.01852, -30.0, 30.0, 4.26),
(2, 3, 0.00297, 0.0297, 0.00674, -30.0, 30.0, 4.26),
(3, 4, 0.00297, 0.0297, 0.00674, -30.0, 30.0, 2.40),
]
buses = [
# (Pd, Qd, Gs, Bs, Vmin, Vmax)
(0.0, 0.0000, 0.0, 0.0, 0.9, 1.1),
(3.0, 0.9861, 0.0, 0.0, 0.9, 1.1),
(3.0, 0.9861, 0.0, 0.0, 0.9, 1.1),
(4.0, 1.3147, 0.0, 0.0, 0.9, 1.1),
(0.0, 0.0000, 0.0, 0.0, 0.9, 1.1),
]
generators = [
# (bus, Pmin, Pmax, Qmin, Qmax, a, b, c)
(0, 0.0, 0.4, -0.300, 0.300, 0.0, 1400, 0.0),
(0, 0.0, 1.7, -1.275, 1.275, 0.0, 1500, 0.0),
(2, 0.0, 5.2, -3.900, 3.900, 0.0, 3000, 0.0),
(3, 0.0, 2.0, -1.500, 1.500, 0.0, 4000, 0.0),
(4, 0.0, 6.0, -4.500, 4.500, 0.0, 1000, 0.0),
]
slack_bus = 3
Then we declare the variables:
import math
import pyoptinterface as poi
from pyoptinterface import nl, ipopt
model = ipopt.Model()
N_branch = len(branches)
N_bus = len(buses)
N_gen = len(generators)
Pbr_from = model.add_m_variables(N_branch)
Qbr_from = model.add_m_variables(N_branch)
Pbr_to = model.add_m_variables(N_branch)
Qbr_to = model.add_m_variables(N_branch)
V = model.add_m_variables(N_bus, name="V")
theta = model.add_m_variables(N_bus, name="theta")
for i in range(N_bus):
Vmin, Vmax = buses[i][4], buses[i][5]
model.set_variable_bounds(V[i], Vmin, Vmax)
model.set_variable_bounds(theta[slack_bus], 0.0, 0.0)
P = model.add_variables(range(N_gen), name="P")
Q = model.add_variables(range(N_gen), name="Q")
for i in range(N_gen):
model.set_variable_bounds(P[i], generators[i][1], generators[i][2])
model.set_variable_bounds(Q[i], generators[i][3], generators[i][4])
Next, we add the constraints:
# nonlinear constraints
for k in range(N_branch):
with nl.graph():
branch = branches[k]
R, X, Bc2 = branch[2], branch[3], branch[4]
G = R / (R**2 + X**2)
B = -X / (R**2 + X**2)
Bc = Bc2 / 2
i = branch[0]
j = branch[1]
Vi = V[i]
Vj = V[j]
theta_i = theta[i]
theta_j = theta[j]
Pij = Pbr_from[k]
Qij = Qbr_from[k]
Pji = Pbr_to[k]
Qji = Qbr_to[k]
sin_ij = nl.sin(theta_i - theta_j)
cos_ij = nl.cos(theta_i - theta_j)
Pij_eq = G * Vi**2 - Vi * Vj * (G * cos_ij + B * sin_ij) - Pij
Qij_eq = -(B + Bc) * Vi**2 - Vi * Vj * (G * sin_ij - B * cos_ij) - Qij
Pji_eq = G * Vj**2 - Vi * Vj * (G * cos_ij - B * sin_ij) - Pji
Qji_eq = -(B + Bc) * Vj**2 - Vi * Vj * (-G * sin_ij - B * cos_ij) - Qji
model.add_nl_constraint(
Pij_eq == 0.0,
)
model.add_nl_constraint(
Qij_eq == 0.0,
)
model.add_nl_constraint(
Pji_eq == 0.0,
)
model.add_nl_constraint(
Qji_eq == 0.0,
)
# power balance constraints
P_balance_eq = [poi.ExprBuilder() for i in range(N_bus)]
Q_balance_eq = [poi.ExprBuilder() for i in range(N_bus)]
for b in range(N_bus):
Pd, Qd = buses[b][0], buses[b][1]
Gs, Bs = buses[b][2], buses[b][3]
Vb = V[b]
P_balance_eq[b] -= poi.quicksum(
Pbr_from[k] for k in range(N_branch) if branches[k][0] == b
)
P_balance_eq[b] -= poi.quicksum(
Pbr_to[k] for k in range(N_branch) if branches[k][1] == b
)
P_balance_eq[b] += poi.quicksum(
P[i] for i in range(N_gen) if generators[i][0] == b
)
P_balance_eq[b] -= Pd
P_balance_eq[b] -= Gs * Vb * Vb
Q_balance_eq[b] -= poi.quicksum(
Qbr_from[k] for k in range(N_branch) if branches[k][0] == b
)
Q_balance_eq[b] -= poi.quicksum(
Qbr_to[k] for k in range(N_branch) if branches[k][1] == b
)
Q_balance_eq[b] += poi.quicksum(
Q[i] for i in range(N_gen) if generators[i][0] == b
)
Q_balance_eq[b] -= Qd
Q_balance_eq[b] += Bs * Vb * Vb
model.add_quadratic_constraint(P_balance_eq[b], poi.Eq, 0.0)
model.add_quadratic_constraint(Q_balance_eq[b], poi.Eq, 0.0)
for k in range(N_branch):
branch = branches[k]
i = branch[0]
j = branch[1]
theta_i = theta[i]
theta_j = theta[j]
angmin = branch[5] / 180 * math.pi
angmax = branch[6] / 180 * math.pi
model.add_linear_constraint(theta_i - theta_j, (angmin, angmax))
Smax = branch[7]
Pij = Pbr_from[k]
Qij = Qbr_from[k]
Pji = Pbr_to[k]
Qji = Qbr_to[k]
model.add_quadratic_constraint(Pij * Pij + Qij * Qij, poi.Leq, Smax * Smax)
model.add_quadratic_constraint(Pji * Pji + Qji * Qji, poi.Leq, Smax * Smax)
Finally, we set the objective function:
cost = poi.ExprBuilder()
for i in range(N_gen):
a, b, c = generators[i][5], generators[i][6], generators[i][7]
cost += a * P[i] * P[i] + b * P[i] + c
model.set_objective(cost)
After optimization, we can retrieve the optimal solution:
model.optimize()
print(model.get_model_attribute(poi.ModelAttribute.TerminationStatus))
P_value = P.map(model.get_value)
print("Optimal active power output of the generators:")
for i in range(N_gen):
print(f"Generator {i}: {P_value[i]}")