Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/architecture/drop_bus_branch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Drop Bus-branch support

In the early days of ToOp, development happened on bus-branch grids (such as UCTE files). However, by now the support for bus/branch grids is mostly gone. Asset topologies require node/breaker information and also the export functionalities are built on node/breaker grids.

Note that dropping bus/branch support will not remove the ability to optimize bus/branch grids alltogether. It only requires a preprocessing routine that amends the bus/branch grid using common assumptions to a node/breaker grid, e.g. by introducing asset bays manually.

## Why we didn't do it

A lot of tests build on bus/branch grids (ieee grids). It would be wise to introduce a general routine that performs bespoke node/breaker amendment for all test grids, so generally all tests run on node/breaker grids. Even though the jax-based tests might not need it everywhere, for comparing against the postprocess runners this would make sense.

## How to deal with pandapower

Pandapower tests are fully bus/branch because we didn't implement the asset topologies properly for node/breaker. We might end up in a situation where it would be much easier to either drop bus/branch AND pandapower support or to keep both in. If such a situation arises, it shall first be checked whether pulling in the asset topology support for pandapower might be feasible. If we end up in a stalemate situation, redecide as dropping pandapower completely is not intended at the moment.
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@
from copy import deepcopy
from pathlib import Path

import pandera.typing as pat
from beartype.typing import Optional, TypeAlias, Union
from fsspec import AbstractFileSystem
from toop_engine_interfaces.asset_topology import RealizedTopology
from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars
from toop_engine_interfaces.nminus1_definition import Nminus1Definition
from toop_engine_interfaces.node_breaker_update import SwitchUpdateSchema
from toop_engine_interfaces.node_breaker_update import NodeBreakerUpdate
from toop_engine_interfaces.stored_action_set import ActionSet

AdditionalActionInfo: TypeAlias = Union[pat.DataFrame[SwitchUpdateSchema], RealizedTopology]
AdditionalActionInfo: TypeAlias = Union[NodeBreakerUpdate, RealizedTopology]


class AbstractLoadflowRunner(ABC):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,11 @@ def is_node_breaker_grid(net: Network, relevant_station: Optional[str] = None) -
)


def is_bus_branch_grid(net: Network, relevant_station: Optional[str] = None) -> bool:
"""Check if the network is in bus-branch format."""
return not is_node_breaker_grid(net=net, relevant_station=relevant_station)


def apply_station(net: Network, station: Station) -> Union[pa.typing.DataFrame[SwitchUpdateSchema], RealizedStation]:
"""Apply a station topology to a powsybl model

Expand All @@ -624,13 +629,14 @@ def apply_station(net: Network, station: Station) -> Union[pa.typing.DataFrame[S
The realized station object which contains the input station plus a diff of switched couplers, reassignments and
disconnections or a dataframe of switches that were updated.
"""
if is_node_breaker_grid(net=net, relevant_station=station.grid_model_id):
return apply_node_breaker_topology(
net=net,
target_topology=Topology(
topology_id="this_id_will_be_ignored",
stations=[station],
timestamp=datetime.now(),
),
)
return apply_station_bus_branch(net=net, station=station)
if is_bus_branch_grid(net=net, relevant_station=station.grid_model_id):
return apply_station_bus_branch(net=net, station=station)

return apply_node_breaker_topology(
net=net,
target_topology=Topology(
topology_id="this_id_will_be_ignored",
stations=[station],
timestamp=datetime.now(),
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@
fingerprint_current_limits_for_powsybl,
get_current_branch_limits_for_powsybl,
)
from toop_engine_dc_solver.export.export import get_node_breaker_updates_from_action_set
from toop_engine_dc_solver.postprocess.abstract_runner import AbstractLoadflowRunner, AdditionalActionInfo
from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import (
apply_node_breaker_topology,
apply_topology_bus_branch,
is_bus_branch_grid,
is_node_breaker_grid,
)
from toop_engine_grid_helpers.powsybl.loadflow_parameters import DISTRIBUTED_SLACK
Expand All @@ -49,6 +50,7 @@
from toop_engine_interfaces.asset_topology_helpers import electrical_components
from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars
from toop_engine_interfaces.nminus1_definition import Contingency, Nminus1Definition
from toop_engine_interfaces.node_breaker_update import NodeBreakerUpdate
from toop_engine_interfaces.stored_action_set import ActionSet

logger = structlog.get_logger(__name__)
Expand Down Expand Up @@ -112,8 +114,56 @@ def get_islanding_contingency_ids(net: Network, nminus1_definition: Nminus1Defin
return islanding_contingency_ids


def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> AdditionalActionInfo | None:
"""Apply actions to a powsybl network
def apply_node_breaker_updates(net: Network, node_breaker_updates: NodeBreakerUpdate) -> NodeBreakerUpdate:
"""Apply raw node-breaker updates to a powsybl network.

Parameters
----------
net : Network
The powsybl network to modify. Will be modified in-place.
node_breaker_updates : NodeBreakerUpdate
Low-level switch and PST updates to apply.

Returns
-------
NodeBreakerUpdate
The applied updates - this will always be exactly the input node_breaker_updates and is just added for convenience.
"""
if len(node_breaker_updates.switch_updates):
net.update_switches(
id=node_breaker_updates.switch_updates["grid_model_id"].tolist(),
open=node_breaker_updates.switch_updates["open"].tolist(),
)
if len(node_breaker_updates.pst_updates):
net.update_phase_tap_changers(
id=node_breaker_updates.pst_updates["grid_model_id"].tolist(),
tap=node_breaker_updates.pst_updates["tap"].tolist(),
)
return node_breaker_updates


def apply_node_breaker_action_set_updates(
net: Network,
actions: list[int],
action_set: ActionSet,
disconnections: Optional[list[int]] = None,
pst_setpoints: Optional[list[int]] = None,
) -> NodeBreakerUpdate | None:
"""Translate action-set indices to raw node-breaker updates and apply them."""
if not len(actions) and not len(disconnections or []) and not len(pst_setpoints or []):
return None

node_breaker_updates = get_node_breaker_updates_from_action_set(
action_set=action_set,
actions=actions,
disconnections=disconnections,
pst_setpoints=pst_setpoints,
)
return apply_node_breaker_updates(net, node_breaker_updates)


def apply_bus_branch_topology(net: Network, actions: list[int], action_set: ActionSet) -> AdditionalActionInfo | None:
"""Apply topology actions to a bus-branch powsybl network.

Parameters
----------
Expand All @@ -128,7 +178,7 @@ def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> A
Returns
-------
AdditionalActionInfo | None
Additional information about the action, either a DataFrame of switch updates or a RealizedTopology.
Realized topology information for the bus-branch application.
Returns None if no actions are provided.
"""
if not len(actions):
Expand All @@ -137,16 +187,27 @@ def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> A
stations = [action_set.local_actions[action] for action in actions]
changed_stations_topo = action_set.starting_topology.model_copy(update={"stations": stations})

if is_node_breaker_grid(net, stations[0].grid_model_id):
additional_info = apply_node_breaker_topology(net, changed_stations_topo)
else:
additional_info = apply_topology_bus_branch(net, changed_stations_topo)
return apply_topology_bus_branch(net, changed_stations_topo)


def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> AdditionalActionInfo | None:
"""Apply topology actions to a powsybl network.

Node-breaker grids are translated to raw switch updates before application. The
legacy bus-branch path stays isolated in ``apply_bus_branch_topology``.
"""
if not len(actions):
return None

relevant_station = action_set.local_actions[actions[0]].grid_model_id
if is_bus_branch_grid(net, relevant_station=relevant_station):
return apply_bus_branch_topology(net, actions, action_set)

return additional_info
return apply_node_breaker_action_set_updates(net, actions, action_set)


def apply_disconnections(net: Network, disconnections: list[int], action_set: ActionSet) -> None:
"""Apply static disconnections to a powsybl network.
def apply_bus_branch_disconnections(net: Network, disconnections: list[int], action_set: ActionSet) -> None:
"""Apply static disconnections to a bus-branch powsybl network.

Works by removing the elements from the network.

Expand All @@ -163,9 +224,7 @@ def apply_disconnections(net: Network, disconnections: list[int], action_set: Ac
Raises
------
RuntimeError
If an element could not be disconnected


If an element could not be disconnected.
"""
for disconnection in disconnections:
assert disconnection < len(action_set.disconnectable_branches), "Disconnection index out of range"
Expand All @@ -174,8 +233,8 @@ def apply_disconnections(net: Network, disconnections: list[int], action_set: Ac
logger.warning(f"Failed to disconnect element {elem.id} of type {elem.kind} at index {disconnection}")


def apply_pst_setpoints(net: Network, pst_setpoints: list[int], action_set: ActionSet) -> None:
"""Apply phase shift tap setpoints to a powsybl network.
def apply_bus_branch_pst_setpoints(net: Network, pst_setpoints: list[int], action_set: ActionSet) -> None:
"""Apply phase shift tap setpoints to a bus-branch powsybl network.

Works by setting the tap position of the controllable PSTs in the network to the given setpoints.

Expand Down Expand Up @@ -442,12 +501,26 @@ def _apply_requested_changes(
None
"""
self.last_action_info = None
if len(actions):
self.last_action_info = apply_topology(net, actions, self.action_set)
if len(disconnections):
apply_disconnections(net, disconnections, self.action_set)
if pst_setpoints is not None and len(pst_setpoints):
apply_pst_setpoints(net, pst_setpoints, self.action_set)
if not len(actions) and not len(disconnections) and not (pst_setpoints is not None and len(pst_setpoints)):
return

assert self.action_set is not None, "Action set must be stored before applying topology changes"
if is_bus_branch_grid(net, relevant_station=self._get_relevant_station_id()):
if len(actions):
self.last_action_info = apply_bus_branch_topology(net, actions, self.action_set)
if len(disconnections):
apply_bus_branch_disconnections(net, disconnections, self.action_set)
if pst_setpoints is not None and len(pst_setpoints):
apply_bus_branch_pst_setpoints(net, pst_setpoints, self.action_set)
return

self.last_action_info = apply_node_breaker_action_set_updates(
net,
actions,
self.action_set,
disconnections,
pst_setpoints,
)

def build_topology_network(
self,
Expand All @@ -458,12 +531,20 @@ def build_topology_network(
"""Build a fresh network copy with the requested topology applied."""
assert self.net is not None, "Base grid must be loaded before building a topology network"
net = deepcopy(self.net)
if len(actions):
_ = apply_topology(net, actions, self.action_set)
if len(disconnections):
apply_disconnections(net, disconnections, self.action_set)
if pst_setpoints is not None and len(pst_setpoints):
apply_pst_setpoints(net, pst_setpoints, self.action_set)
if not len(actions) and not len(disconnections) and not (pst_setpoints is not None and len(pst_setpoints)):
return net

assert self.action_set is not None, "Action set must be stored before building a topology network"
if is_bus_branch_grid(net, relevant_station=self._get_relevant_station_id()):
if len(actions):
_ = apply_bus_branch_topology(net, actions, self.action_set)
if len(disconnections):
apply_bus_branch_disconnections(net, disconnections, self.action_set)
if pst_setpoints is not None and len(pst_setpoints):
apply_bus_branch_pst_setpoints(net, pst_setpoints, self.action_set)
return net

_ = apply_node_breaker_action_set_updates(net, actions, self.action_set, disconnections, pst_setpoints)
return net

@overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from toop_engine_dc_solver.jax.types import ActionIndexComputations, NodalInjOptimResults, NodalInjStartOptions
from toop_engine_dc_solver.postprocess.postprocess_powsybl import (
PowsyblRunner,
apply_disconnections,
apply_bus_branch_disconnections,
apply_topology,
compute_cross_coupler_flows,
)
Expand Down Expand Up @@ -89,7 +89,7 @@ def test_apply_topology(preprocessed_powsybl_data_folder: Path) -> None:
assert dc_res[0].status == pypowsybl.loadflow.ComponentStatus.CONVERGED


def test_apply_disconnections(preprocessed_powsybl_data_folder: Path) -> None:
def test_apply_bus_branch_disconnections(preprocessed_powsybl_data_folder: Path) -> None:
net = pypowsybl.network.load(preprocessed_powsybl_data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"])

assert net.get_branches().connected1.iloc[0]
Expand All @@ -101,7 +101,7 @@ def test_apply_disconnections(preprocessed_powsybl_data_folder: Path) -> None:

disconnections = [0]

apply_disconnections(net, disconnections, action_set)
apply_bus_branch_disconnections(net, disconnections, action_set)

assert not net.get_branches().connected1.iloc[0]
assert not net.get_branches().connected2.iloc[0]
Expand Down Expand Up @@ -423,8 +423,8 @@ def test_powsybl_runner_reuses_branch_limit_cache_for_contingency_only_updates(
net = pypowsybl.network.load(preprocessed_powsybl_data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"])

monkeypatch.setattr(
"toop_engine_dc_solver.postprocess.postprocess_powsybl.is_node_breaker_grid",
lambda *_args, **_kwargs: False,
"toop_engine_dc_solver.postprocess.postprocess_powsybl.is_bus_branch_grid",
lambda *_args, **_kwargs: True,
)
monkeypatch.setattr(
"toop_engine_dc_solver.postprocess.postprocess_powsybl.get_current_branch_limits_for_powsybl",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from dataclasses import dataclass

import numpy as np
import pandas as pd
import polars as pl
import structlog
from beartype.typing import Collection, Optional
Expand All @@ -26,6 +25,7 @@
from toop_engine_interfaces.loadflow_results import ConvergenceStatus
from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars
from toop_engine_interfaces.nminus1_definition import Nminus1Definition
from toop_engine_interfaces.node_breaker_update import NodeBreakerUpdate
from toop_engine_topology_optimizer.ac.evolution_functions import INF_FITNESS, get_contingency_indices_from_ids
from toop_engine_topology_optimizer.ac.storage import ACOptimTopology
from toop_engine_topology_optimizer.ac.types import EarlyStoppingStageResult, RunnerGroup, TopologyScoringResult
Expand Down Expand Up @@ -167,8 +167,8 @@ def extract_switching_distance(additional_info: AdditionalActionInfo) -> int:
"""
if isinstance(additional_info, RealizedTopology):
return len(additional_info.reassignment_diff)
if isinstance(additional_info, pd.DataFrame):
return len(additional_info)
if isinstance(additional_info, NodeBreakerUpdate):
return len(additional_info.switch_updates)
raise ValueError("Unknown format for additional info")


Expand Down
Loading
Loading