Swing-equation power grid with Braess paradox
A ring power grid in the swing-equation model, where more transmission capacity can break synchrony.
- Name:
power_grid - Field:
engineering/power_systems - Formalism: ordinary differential equations
- References: Witthaut and Timme 2012; Datseris and Parlitz 2022
Factory
@register_model(
field='engineering/power_systems',
tags=('power grid', 'synchronization', 'network', 'braess paradox', 'ode'),
references=('Witthaut and Timme 2012', 'Datseris and Parlitz 2022'),
defaults={'until': 300.0, 'dt': 1.0},
)
def power_grid(
n_nodes: int = 8,
*,
K: float = 2.75,
power: float = 1.0,
gamma: float = 0.1,
added_line: tuple[int, int] | None = None,
doubled_line: tuple[int, int] | None = None,
) -> System:
"""Return a ring power grid in the swing-equation model, where more transmission capacity can break synchrony.
The phase of node `i` relative to the grid frequency follows
`phi_i'' = P_i - gamma phi_i' + Σ_j K_ij sin(phi_j - phi_i)`.
The first half of the ring are generators with `P = power` and the second half consumers with `P = -power`, joined
by lines of capacity `K`. The default grid synchronises, but adding the line `(0, 2)` or doubling the capacity of
the line `(0, 1)` makes the flows overload a line, and synchrony is lost, which is Braess's paradox.
Args:
n_nodes:
The number of nodes of the ring.
K:
The capacity of every line.
power:
The power of every generator, and its negative the power of every consumer.
gamma:
The damping.
added_line:
A pair of nodes to join by an extra line of capacity `K`, or `None`.
doubled_line:
A pair of neighbouring nodes whose line has capacity `2 K`, or `None`.
Returns:
system:
The model, with the phases `node_<i>.phi` and frequency deviations `node_<i>.phi_d1`.
Examples:
>>> from skcomplex.models.engineering.power_systems import power_grid
>>> len(power_grid(added_line=(0, 2)).interactions)
9
"""
elements = [
GridNodeElement(
f'node_{index}',
phi=0.0,
phi_d1=0.0,
P=power if index < n_nodes // 2 else -power,
gamma=gamma,
)
for index in range(n_nodes)
]
pairs = [(index, (index + 1) % n_nodes) for index in range(n_nodes)]
doubled = None if doubled_line is None else frozenset(doubled_line)
interactions = [
TransmissionLineInteraction(
f'line_{first}_{second}',
ends=[f'node_{first}', f'node_{second}'],
K=2 * K if frozenset((first, second)) == doubled else K,
)
for first, second in pairs
]
if added_line is not None:
first, second = added_line
interactions.append(
TransmissionLineInteraction(f'line_{first}_{second}', ends=[f'node_{first}', f'node_{second}'], K=K),
)
return System(elements=elements, interactions=interactions)
Components
class GridNodeElement(Element):
"""Node of a power grid whose phase follows the swing equation with a power and a damping."""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
phi: Annotated[object, StateSpec(order=2)] = 0.0,
phi_d1: float = 0.0,
P: Annotated[object, ParameterSpec()] = 1.0,
gamma: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
super().__init__(label, visual=visual)
self.phi = phi
self.phi_d1 = phi_d1
self.P = P
self.gamma = gamma
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the swing equation of the phase."""
return [RateLaw(s.phi, s.P - s.gamma * derivative(s.phi, 1))]
class TransmissionLineInteraction(CouplingInteraction):
"""Transmission line of a capacity that couples the phases of two nodes by the sine of their difference.
It is the coupling of the two nodes as separate elements, each getting `K sin(phi_j - phi_i)` from the other.
Args:
label:
The label of the interaction.
ends:
The paths of the two nodes the line joins.
K:
The capacity of the line.
"""
def __init__(
self,
label: str,
*,
ends: Annotated[Sequence[str], RoleSpec()],
K: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
super().__init__(label, target=ends, read='phi', write='phi', function=self._get_flow, mode='group')
self.ends = ends
self.K = K
@staticmethod
def _get_flow(namespace: SimpleNamespace) -> Target:
"""Return the power flow from the other end of the line, `K sin(phi_j - phi_i)`."""
return namespace.K * sin(namespace.other.phi - namespace.member.phi)
Equations
node_0.phi: diff(phi_node_0(t), t, t) == K_line_0_1*sin(-phi_node_0(t) + phi_node_1(t)) + K_line_7_0*sin(-phi_node_0(t) + phi_node_7(t)) - gamma_node_0*diff(phi_node_0(t), t) + P_node_0
node_1.phi: diff(phi_node_1(t), t, t) == K_line_0_1*sin(phi_node_0(t) - phi_node_1(t)) + K_line_1_2*sin(-phi_node_1(t) + phi_node_2(t)) - gamma_node_1*diff(phi_node_1(t), t) + P_node_1
node_2.phi: diff(phi_node_2(t), t, t) == K_line_1_2*sin(phi_node_1(t) - phi_node_2(t)) + K_line_2_3*sin(-phi_node_2(t) + phi_node_3(t)) - gamma_node_2*diff(phi_node_2(t), t) + P_node_2
node_3.phi: diff(phi_node_3(t), t, t) == K_line_2_3*sin(phi_node_2(t) - phi_node_3(t)) + K_line_3_4*sin(-phi_node_3(t) + phi_node_4(t)) - gamma_node_3*diff(phi_node_3(t), t) + P_node_3
node_4.phi: diff(phi_node_4(t), t, t) == K_line_3_4*sin(phi_node_3(t) - phi_node_4(t)) + K_line_4_5*sin(-phi_node_4(t) + phi_node_5(t)) - gamma_node_4*diff(phi_node_4(t), t) + P_node_4
node_5.phi: diff(phi_node_5(t), t, t) == K_line_4_5*sin(phi_node_4(t) - phi_node_5(t)) + K_line_5_6*sin(-phi_node_5(t) + phi_node_6(t)) - gamma_node_5*diff(phi_node_5(t), t) + P_node_5
node_6.phi: diff(phi_node_6(t), t, t) == K_line_5_6*sin(phi_node_5(t) - phi_node_6(t)) + K_line_6_7*sin(-phi_node_6(t) + phi_node_7(t)) - gamma_node_6*diff(phi_node_6(t), t) + P_node_6
node_7.phi: diff(phi_node_7(t), t, t) == K_line_7_0*sin(phi_node_0(t) - phi_node_7(t)) + K_line_6_7*sin(phi_node_6(t) - phi_node_7(t)) - gamma_node_7*diff(phi_node_7(t), t) + P_node_7