diff --git a/.pre-commit-hooks/sensitive-files-whitelist.txt b/.pre-commit-hooks/sensitive-files-whitelist.txt
index 00d6e449f..bfe4d1f5e 100644
--- a/.pre-commit-hooks/sensitive-files-whitelist.txt
+++ b/.pre-commit-hooks/sensitive-files-whitelist.txt
@@ -49,6 +49,10 @@ packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.zip
packages/dc_solver_pkg/tests/files/test_grid_node_breaker/nminus1_definition.json
packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/test_ucte_powsybl_example.xiidm
packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/asset_topology.json
+packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set_diffs.hdf5
+packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/original_gridfile/grid.xiidm
+packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json
+packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information_stats.json
# Grid helpers package test files
packages/grid_helpers_pkg/tests/files/test_ucte_powsybl_example.uct
diff --git a/packages/contingency_analysis_pkg/tests/powsybl/test_ca_powsybl_helpers.py b/packages/contingency_analysis_pkg/tests/powsybl/test_ca_powsybl_helpers.py
index 89720a88c..0e829813d 100644
--- a/packages/contingency_analysis_pkg/tests/powsybl/test_ca_powsybl_helpers.py
+++ b/packages/contingency_analysis_pkg/tests/powsybl/test_ca_powsybl_helpers.py
@@ -147,6 +147,27 @@ def test_translate_contingency_to_powsybl():
assert pow_basecase[0].id == basecase_contingency[0].id, "Id should stay the same"
+def test_translate_contingency_to_powsybl_with_busbar(
+ powsybl_node_breaker_net: pypowsybl.network.Network,
+) -> None:
+ identifiables = powsybl_node_breaker_net.get_identifiables(attributes=[]).index
+ busbar_id = powsybl_node_breaker_net.get_busbar_sections(attributes=[]).index[0]
+ contingency = Contingency(
+ id=busbar_id,
+ elements=[GridElement(id=busbar_id, type="BUSBAR_SECTION", kind="bus")],
+ )
+
+ pow_contingencies, missing_contingencies = translate_contingency_to_powsybl(
+ [contingency],
+ identifiables=identifiables,
+ )
+
+ assert missing_contingencies == []
+ assert len(pow_contingencies) == 1
+ assert pow_contingencies[0].id == busbar_id
+ assert pow_contingencies[0].elements == [busbar_id]
+
+
def test_translate_monitored_elements_to_powsybl(powsybl_bus_breaker_net: pypowsybl.network.Network) -> None:
branches = powsybl_bus_breaker_net.get_branches()
switches = powsybl_bus_breaker_net.get_switches()
@@ -574,6 +595,38 @@ def test_translate_nminus1_for_powsybl_split_helpers(powsybl_bus_breaker_net: py
assert translated_full.voltage_levels.equals(translated_rest.voltage_levels)
+def test_translate_nminus1_for_powsybl_keeps_busbar_contingencies(
+ powsybl_node_breaker_net: pypowsybl.network.Network,
+) -> None:
+ busbar_sections = powsybl_node_breaker_net.get_busbar_sections(attributes=["name"])
+ selected_busbar_id, selected_busbar = next(busbar_sections.iterrows())
+ nminus1_def = Nminus1Definition(
+ monitored_elements=[],
+ contingencies=[
+ Contingency(id="BASECASE", elements=[]),
+ Contingency(
+ id=selected_busbar_id,
+ name=selected_busbar.name or "",
+ elements=[
+ GridElement(
+ id=selected_busbar_id,
+ name=selected_busbar.name or "",
+ type="BUSBAR_SECTION",
+ kind="bus",
+ )
+ ],
+ ),
+ ],
+ id_type="powsybl",
+ )
+
+ translated_nminus1 = translate_nminus1_components_for_powsybl(nminus1_def, powsybl_node_breaker_net)
+
+ assert [contingency.id for contingency in translated_nminus1.contingencies] == ["BASECASE", selected_busbar_id]
+ assert translated_nminus1.contingencies[1].elements == [selected_busbar_id]
+ assert translated_nminus1.missing_contingencies == []
+
+
def test_translate_nminus1_for_powsybl_with_branch_limit_cache(
powsybl_bus_breaker_net: pypowsybl.network.Network,
) -> None:
diff --git a/packages/contingency_analysis_pkg/tests/powsybl/test_contingency_analysis_powsybl.py b/packages/contingency_analysis_pkg/tests/powsybl/test_contingency_analysis_powsybl.py
index c700395aa..80aa1e9cc 100644
--- a/packages/contingency_analysis_pkg/tests/powsybl/test_contingency_analysis_powsybl.py
+++ b/packages/contingency_analysis_pkg/tests/powsybl/test_contingency_analysis_powsybl.py
@@ -5,9 +5,12 @@
# you can obtain one at https://mozilla.org/MPL/2.0/.
# Mozilla Public License, version 2.0
+from dataclasses import replace
+
import numpy as np
import pandas as pd
import pandera as pa
+import polars as pl
import pypowsybl
import pytest
from polars.testing import assert_frame_equal
@@ -22,7 +25,12 @@
build_branch_limit_cache,
run_contingency_analysis_powsybl,
)
-from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK
+from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK, SINGLE_SLACK
+from toop_engine_importer.network_graph.powsybl_station_to_graph import (
+ get_relevant_voltage_levels,
+ get_station_list,
+)
+from toop_engine_importer.pypowsybl_import import powsybl_masks
from toop_engine_interfaces.loadflow_result_helpers import (
convert_pandas_loadflow_results_to_polars,
convert_polars_loadflow_results_to_pandas,
@@ -32,6 +40,35 @@
from toop_engine_interfaces.nminus1_definition import Contingency, GridElement, MonitoredElement, Nminus1Definition
+def _normalize_contingency_frame(
+ frame: pl.DataFrame,
+ contingency_id: str,
+ excluded_element_prefixes: set[str] | None = None,
+ excluded_elements: set[str] | None = None,
+ excluded_branches: set[str] | None = None,
+) -> pl.DataFrame:
+ """Normalize contingency-specific result frames for direct equality checks."""
+ normalized = frame.filter(pl.col("contingency") == contingency_id)
+
+ if excluded_element_prefixes and "element" in normalized.columns:
+ excluded_prefix_expr = pl.lit(False)
+ for prefix in sorted(excluded_element_prefixes):
+ excluded_prefix_expr = excluded_prefix_expr | pl.col("element").str.starts_with(prefix)
+ normalized = normalized.filter(~excluded_prefix_expr)
+
+ if excluded_elements and "element" in normalized.columns:
+ normalized = normalized.filter(~pl.col("element").is_in(sorted(excluded_elements)))
+
+ if excluded_branches and {"element", "side"}.issubset(set(normalized.columns)):
+ normalized = normalized.filter(~pl.col("element").is_in(sorted(excluded_branches)))
+
+ if "contingency_name" in normalized.columns:
+ normalized = normalized.with_columns(pl.lit("").alias("contingency_name"))
+ normalized = normalized.with_columns(pl.lit("reference").alias("contingency"))
+ sort_columns = [column for column in normalized.columns if column not in {"contingency_name"}]
+ return normalized.sort(sort_columns)
+
+
def test_run_powsybl_analysis(powsybl_bus_breaker_net: pypowsybl.network.Network) -> None:
nminus1_definition = get_full_nminus1_definition_powsybl(powsybl_bus_breaker_net)
@@ -160,6 +197,235 @@ def test_contingency_analysis_validated_or_not(powsybl_node_breaker_net: pypowsy
assert lf_result_with_val_polars == lf_result_no_val_polars
+def test_run_ac_contingency_analysis_powsybl_with_busbar_outage(
+ powsybl_node_breaker_net: pypowsybl.network.Network,
+) -> None:
+ nminus1_definition = get_full_nminus1_definition_powsybl(powsybl_node_breaker_net)
+ busbar_sections = powsybl_node_breaker_net.get_busbar_sections()
+ # select a busbar section that is not the slack
+ lf_res = pypowsybl.loadflow.run_ac(powsybl_node_breaker_net)[0]
+ busbar_sections = busbar_sections[busbar_sections.bus_id != lf_res.reference_bus_id]
+ selected_busbar_id = selected_busbar_name = busbar_sections.index[0]
+ nminus1_definition.contingencies.append(
+ Contingency(
+ id=selected_busbar_id,
+ name=selected_busbar_name or "",
+ elements=[
+ GridElement(
+ id=selected_busbar_id,
+ name=selected_busbar_name or "",
+ type="BUSBAR_SECTION",
+ kind="bus",
+ )
+ ],
+ )
+ )
+
+ lf_result = get_ac_loadflow_results(powsybl_node_breaker_net, nminus1_definition, job_id="test_job", n_processes=1)
+
+ converged = lf_result.converged.collect()
+ assert selected_busbar_id in converged["contingency"].to_list()
+ assert not any(selected_busbar_id in warning for warning in lf_result.warnings)
+
+
+def test_busbar_outage_equals_connected_element_outage(
+ powsybl_node_breaker_net: pypowsybl.network.Network,
+) -> None:
+ busbar_sections = powsybl_node_breaker_net.get_busbar_sections()
+ lf_res = pypowsybl.loadflow.run_ac(powsybl_node_breaker_net)[0]
+ non_slack_busbar_sections = busbar_sections[busbar_sections.bus_id != lf_res.reference_bus_id]
+ assert len(non_slack_busbar_sections) > 0
+
+ selected_voltage_levels = {"VL2", "VL3"}
+ selected_busbar_sections = non_slack_busbar_sections[
+ non_slack_busbar_sections["voltage_level_id"].astype(str).isin(selected_voltage_levels)
+ ]
+ assert len(selected_busbar_sections) > 0
+
+ network_masks = powsybl_masks.create_default_network_masks(powsybl_node_breaker_net)
+ network_masks = replace(
+ network_masks,
+ relevant_subs=np.ones_like(network_masks.relevant_subs, dtype=bool),
+ busbar_for_nminus1=np.ones_like(network_masks.busbar_for_nminus1, dtype=bool),
+ )
+ relevant_voltage_level_with_region = get_relevant_voltage_levels(
+ network=powsybl_node_breaker_net,
+ network_masks=network_masks,
+ )
+ station_list = get_station_list(
+ network=powsybl_node_breaker_net,
+ relevant_voltage_level_with_region=relevant_voltage_level_with_region,
+ )
+ assert len(station_list) == len(powsybl_node_breaker_net.get_buses())
+
+ monitored_votlages = []
+ for vl in relevant_voltage_level_with_region["voltage_level_id"].values:
+ bus_breaker_topology = powsybl_node_breaker_net.get_bus_breaker_topology(vl)
+ elements = bus_breaker_topology.elements
+ elements.rename(columns={"bus_id": "bus_breaker_id"}, inplace=True)
+ elements = elements.merge(
+ powsybl_node_breaker_net.get_bus_breaker_view_buses()[["bus_id"]],
+ left_on="bus_breaker_id",
+ right_on="id",
+ how="left",
+ )
+ elements = elements[elements["type"] == "BUSBAR_SECTION"]
+ monitored_votlages += elements["bus_breaker_id"].to_list()
+
+ branches = powsybl_node_breaker_net.get_branches(attributes=["type"])
+ injections = powsybl_node_breaker_net.get_injections(attributes=["type"])
+
+ monitored_elements = get_full_nminus1_definition_powsybl(powsybl_node_breaker_net).monitored_elements
+ # filter monitored bus elements to only monitor buses from powsybl_node_breaker_net.get_buses()
+ monitored_elements = [
+ element
+ for element in monitored_elements
+ if (element.type == "BUS" and element.id in monitored_votlages) or (element.type != "BUS")
+ ]
+
+ for selected_busbar_id in selected_busbar_sections.index:
+ selected_busbar_name = selected_busbar_id
+ selected_voltage_level_id = str(selected_busbar_sections.loc[selected_busbar_id, "voltage_level_id"])
+
+ selected_station = next(
+ station
+ for station in station_list
+ if any(busbar.grid_model_id == selected_busbar_id for busbar in station.busbars)
+ )
+ selected_busbar_index = next(
+ i for i, busbar in enumerate(selected_station.busbars) if busbar.grid_model_id == selected_busbar_id
+ )
+ connected_asset_ids = {
+ asset.grid_model_id
+ for i, asset in enumerate(selected_station.assets)
+ if selected_station.asset_switching_table[selected_busbar_index, i]
+ }
+
+ explicit_elements = [
+ GridElement(id=asset_id, name=asset_id, type=branches.loc[asset_id, "type"], kind="branch")
+ for asset_id in connected_asset_ids
+ if asset_id in branches.index
+ ] + [
+ GridElement(
+ id=asset_id,
+ name=asset_id,
+ type=injections.loc[asset_id, "type"],
+ kind="injection",
+ )
+ for asset_id in connected_asset_ids
+ if asset_id in injections.index and injections.loc[asset_id, "type"] != "BUSBAR_SECTION"
+ ]
+
+ nminus1_definition = Nminus1Definition(
+ monitored_elements=monitored_elements,
+ contingencies=[
+ Contingency(id="BASECASE", elements=[]),
+ Contingency(
+ id=selected_busbar_id,
+ name=selected_busbar_name or "",
+ elements=[
+ GridElement(
+ id=selected_busbar_id,
+ name=selected_busbar_name or "",
+ type="BUSBAR_SECTION",
+ kind="bus",
+ )
+ ],
+ ),
+ Contingency(id="explicit_busbar_outage", elements=explicit_elements),
+ ],
+ id_type="powsybl",
+ )
+
+ lf_result = get_ac_loadflow_results(
+ powsybl_node_breaker_net, nminus1_definition, job_id="test_job", n_processes=1, lf_params=SINGLE_SLACK
+ )
+
+ selected_node_elements = set(
+ lf_result.node_results.collect().filter(pl.col("contingency") == selected_busbar_id)["element"].to_list()
+ )
+ explicit_node_elements = set(
+ lf_result.node_results.collect().filter(pl.col("contingency") == "explicit_busbar_outage")["element"].to_list()
+ )
+ excluded_selected_voltage_level_elements = {
+ element_id
+ for element_id in (explicit_node_elements - selected_node_elements)
+ if element_id.startswith(f"{selected_voltage_level_id}_")
+ }
+
+ selected_regulating_elements = set(
+ lf_result.regulating_element_results.collect()
+ .filter(pl.col("contingency") == selected_busbar_id)["element"]
+ .to_list()
+ )
+ explicit_regulating_elements = set(
+ lf_result.regulating_element_results.collect()
+ .filter(pl.col("contingency") == "explicit_busbar_outage")["element"]
+ .to_list()
+ )
+ excluded_explicit_regulating_elements = explicit_regulating_elements - selected_regulating_elements
+
+ selected_va_diff_elements = set(
+ lf_result.va_diff_results.collect().filter(pl.col("contingency") == selected_busbar_id)["element"].to_list()
+ )
+ explicit_va_diff_elements = set(
+ lf_result.va_diff_results.collect()
+ .filter(pl.col("contingency") == "explicit_busbar_outage")["element"]
+ .to_list()
+ )
+ excluded_explicit_va_diff_elements = explicit_va_diff_elements - selected_va_diff_elements
+
+ excluded_branch_ids = {asset_id for asset_id in connected_asset_ids if asset_id in branches.index}
+
+ assert_frame_equal(
+ _normalize_contingency_frame(
+ lf_result.branch_results.collect(),
+ selected_busbar_id,
+ excluded_branches=excluded_branch_ids,
+ ),
+ _normalize_contingency_frame(
+ lf_result.branch_results.collect(),
+ "explicit_busbar_outage",
+ excluded_branches=excluded_branch_ids,
+ ),
+ check_row_order=False,
+ check_exact=False,
+ abs_tol=1e-4,
+ )
+ assert_frame_equal(
+ _normalize_contingency_frame(lf_result.node_results.collect(), selected_busbar_id),
+ _normalize_contingency_frame(
+ lf_result.node_results.collect(),
+ "explicit_busbar_outage",
+ excluded_elements=excluded_selected_voltage_level_elements,
+ ),
+ check_row_order=False,
+ )
+ assert_frame_equal(
+ _normalize_contingency_frame(lf_result.regulating_element_results.collect(), selected_busbar_id),
+ _normalize_contingency_frame(
+ lf_result.regulating_element_results.collect(),
+ "explicit_busbar_outage",
+ excluded_elements=excluded_explicit_regulating_elements,
+ ),
+ check_row_order=False,
+ )
+ assert_frame_equal(
+ _normalize_contingency_frame(lf_result.va_diff_results.collect(), selected_busbar_id),
+ _normalize_contingency_frame(
+ lf_result.va_diff_results.collect(),
+ "explicit_busbar_outage",
+ excluded_elements=excluded_explicit_va_diff_elements,
+ ),
+ check_row_order=False,
+ )
+ assert_frame_equal(
+ _normalize_contingency_frame(lf_result.converged.collect(), selected_busbar_id),
+ _normalize_contingency_frame(lf_result.converged.collect(), "explicit_busbar_outage"),
+ check_row_order=False,
+ )
+
+
@pytest.mark.parametrize("powsybl_net", ["powsybl_bus_breaker_net", "powsybl_node_breaker_net"])
def test_contingency_analysis_ray_vs_powsybl(powsybl_net: str, request, init_ray) -> None:
net = request.getfixturevalue(powsybl_net)
diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_grids.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_grids.py
index f513a69d5..5ef23f0c2 100644
--- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_grids.py
+++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_grids.py
@@ -41,6 +41,7 @@
create_busbar_b_in_ieee,
create_complex_grid_battery_hvdc_svc_3w_trafo,
extract_station_info_powsybl,
+ grouped_pst_grid_example,
parallel_pst_example,
powsybl_case30_with_psts,
powsybl_case1354,
@@ -1226,3 +1227,54 @@ def parallel_pst_data_folder(folder: Path) -> NetworkData:
)
return network_data
+
+
+def grouped_pst_grid_example_data_folder(folder: Path, split_group_station: bool = False) -> NetworkData:
+ """Create a preprocessed folder for grouped_pst_example().
+
+ Runs the importer and preprocessing.
+
+ Parameter:
+ folder: Path
+ The root folder where the data is saved to.
+ split_group_station: bool
+ Whether to split the group station into two separate stations.
+ Returns:
+ NetworkData
+ The network data after preprocessing, which can be used for testing the consistency of the preprocessing step
+ """
+ net = grouped_pst_grid_example(linear_pst=None)
+ if split_group_station:
+ net.open_switch("VL2_BREAKER#0")
+ pypowsybl.loadflow.run_dc(net, CGMES_DISTRIBUTED_SLACK)
+
+ grid_file_path = folder / PREPROCESSING_PATHS["grid_file_path_powsybl"]
+ grid_file_path.parent.mkdir(parents=True, exist_ok=True)
+ net.save(grid_file_path)
+
+ importer_parameters = CgmesImporterParameters(
+ grid_model_file=folder / PREPROCESSING_PATHS["grid_file_path_powsybl"],
+ data_folder=folder,
+ area_settings=AreaSettings(
+ cutoff_voltage=1,
+ control_area=[""],
+ view_area=[""],
+ nminus1_area=[""],
+ dso_trafo_factors=LimitAdjustmentParameters(),
+ dso_trafo_weight=1.0,
+ border_line_factors=LimitAdjustmentParameters(),
+ border_line_weight=1.0,
+ ),
+ )
+
+ _ = preprocessing.convert_file(importer_parameters=importer_parameters)
+
+ preprocessing_parameters = PreprocessParameters(action_set_clip=2**4, preprocess_bb_outages=False)
+ _info, _static_information, network_data = load_grid(
+ data_folder_dirfs=DirFileSystem(folder),
+ pandapower=False,
+ status_update_fn=None,
+ parameters=preprocessing_parameters,
+ )
+
+ return network_data
diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/compute_batch.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/compute_batch.py
index 23dacdce4..01e73bc85 100644
--- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/compute_batch.py
+++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/compute_batch.py
@@ -208,12 +208,12 @@ def compute_bsdf_lodf_static_flows(
topo_res.success[:, None],
(topo_res.success.shape[0], dynamic_information.n_inj_failures),
)
+ n_bb_outage_failures = (
+ dynamic_information.n_bb_outages if solver_config.enable_bb_outages and solver_config.bb_outage_as_nminus1 else 0
+ )
bb_outage_success = jnp.broadcast_to(
topo_res.success[:, None],
- (
- topo_res.success.shape[0],
- dynamic_information.n_bb_outages if dynamic_information.bb_outage_baseline_analysis is None else 0,
- ),
+ (topo_res.success.shape[0], n_bb_outage_failures),
)
contingency_success = jnp.concatenate(
[
diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/convert_to_jax.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/convert_to_jax.py
index 94b38d31f..4d46f272c 100644
--- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/convert_to_jax.py
+++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/convert_to_jax.py
@@ -458,7 +458,15 @@ def convert_rel_bb_outage_data( # noqa: C901
# Determine dimensions for padding of branch_outage_set
n_actions = sum(actions_per_sub) # Total number of combinations
- n_max_bb_to_outage_per_sub = max(len(combi) for sub in rel_bb_outage_br_indices for combi in sub)
+ n_max_bb_slots_from_outages = max(len(combi) for sub in rel_bb_outage_br_indices for combi in sub)
+ n_max_bb_slots_from_articulation = max(
+ (
+ max((max(articulation_bbs, default=-1) + 1) for articulation_bbs in sub_articulation_nodes)
+ for sub_articulation_nodes in network_data.rel_bb_articulation_nodes
+ ),
+ default=0,
+ )
+ n_max_bb_to_outage_per_sub = max(n_max_bb_slots_from_outages, n_max_bb_slots_from_articulation)
max_branches_per_sub = max(len(branches) for branches in network_data.branches_at_nodes)
# Initialize the padded array with a sentinel value (int_max)
diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/network_data.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/network_data.py
index 5b1580a7d..89506dbd5 100644
--- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/network_data.py
+++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/network_data.py
@@ -10,6 +10,7 @@
from dataclasses import dataclass
from pathlib import Path
+import networkx as nx
import numpy as np
import pypowsybl
from beartype.typing import NamedTuple, Optional, Sequence, Union
@@ -638,6 +639,84 @@ def find_station(stations: list[Station], grid_model_id: str, fallback: Optional
return [find_station(network_data.simplified_asset_topology.stations, node_id) for node_id in relevant_node_ids]
+def _get_station_articulation_busbar_ids(station: Station) -> set[str]:
+ """Return busbar ids that would split the station if they were outaged."""
+ busbar_intid_index_mapper = {busbar.int_id: index for index, busbar in enumerate(station.busbars)}
+ edges = [
+ (
+ busbar_intid_index_mapper[coupler.busbar_from_id],
+ busbar_intid_index_mapper[coupler.busbar_to_id],
+ )
+ for coupler in station.couplers
+ if (not coupler.open) and coupler.in_service
+ ]
+ if len(edges) <= 1:
+ return set()
+
+ graph = nx.Graph()
+ graph.add_nodes_from(range(len(station.busbars)))
+ graph.add_edges_from(edges)
+ return {station.busbars[node_index].grid_model_id for node_index in nx.articulation_points(graph)}
+
+
+def extract_busbar_outage_ids(network_data: NetworkData) -> list[str]:
+ """Extract busbar outage ids that remain after preprocessing-side filtering."""
+ if network_data.busbar_outage_map is None or network_data.asset_topology is None:
+ return []
+
+ relevant_station_ids = {
+ node_id
+ for node_id, is_relevant in zip(network_data.node_ids, network_data.relevant_node_mask, strict=True)
+ if is_relevant
+ }
+
+ busbar_outage_ids: list[str] = []
+ relevant_stations = get_relevant_stations(network_data)
+ for station_index, station in enumerate(relevant_stations):
+ configured_busbars = set(network_data.busbar_outage_map.get(station.grid_model_id, []))
+ always_articulation_ids: set[str] = set()
+ if network_data.rel_bb_articulation_nodes is not None and station_index < len(
+ network_data.rel_bb_articulation_nodes
+ ):
+ articulation_by_action = network_data.rel_bb_articulation_nodes[station_index]
+ if articulation_by_action:
+ always_articulation_indices = set(articulation_by_action[0])
+ for articulation_indices in articulation_by_action[1:]:
+ always_articulation_indices &= set(articulation_indices)
+ always_articulation_ids = {
+ station.busbars[busbar_index].grid_model_id
+ for busbar_index in always_articulation_indices
+ if busbar_index < len(station.busbars)
+ }
+
+ busbar_outage_ids.extend(
+ busbar.grid_model_id
+ for busbar in station.busbars
+ if busbar.grid_model_id in configured_busbars and busbar.grid_model_id not in always_articulation_ids
+ )
+
+ articulation_ids_by_station = {
+ station.grid_model_id: _get_station_articulation_busbar_ids(station)
+ for station in (
+ network_data.simplified_asset_topology.stations
+ if network_data.simplified_asset_topology is not None
+ else network_data.asset_topology.stations
+ )
+ }
+ for station in network_data.asset_topology.stations:
+ if station.grid_model_id in relevant_station_ids:
+ continue
+ configured_busbars = set(network_data.busbar_outage_map.get(station.grid_model_id, []))
+ articulation_ids = articulation_ids_by_station.get(station.grid_model_id, set())
+ busbar_outage_ids.extend(
+ busbar.grid_model_id
+ for busbar in station.busbars
+ if busbar.grid_model_id in configured_busbars and busbar.grid_model_id not in articulation_ids
+ )
+
+ return busbar_outage_ids
+
+
def map_branch_injection_ids(
network_data: NetworkData,
) -> tuple[list[list[str]], list[list[str]]]:
@@ -871,11 +950,34 @@ def extract_nminus1_definition(network_data: NetworkData) -> Nminus1Definition:
for index in network_data.rel_io_global_inj_index
]
+ busbar_contingencies: list[Contingency] = []
+ if network_data.asset_topology is not None:
+ busbar_lookup = {
+ busbar.grid_model_id: busbar for station in network_data.asset_topology.stations for busbar in station.busbars
+ }
+ busbar_contingencies = [
+ Contingency(
+ elements=[
+ GridElement(
+ id=busbar_id,
+ type=busbar_lookup[busbar_id].type,
+ name=busbar_lookup[busbar_id].name or "",
+ kind="bus",
+ )
+ ],
+ id=busbar_id,
+ name=busbar_lookup[busbar_id].name or "",
+ )
+ for busbar_id in extract_busbar_outage_ids(network_data)
+ if busbar_id in busbar_lookup
+ ]
+
return Nminus1Definition(
monitored_elements=monitored_branches + monitored_nodes + monitored_switches,
contingencies=basecase_contingency
+ branch_contingencies
+ multi_contingencies
+ nonrel_inj_contingencies
- + rel_inj_contingencies,
+ + rel_inj_contingencies
+ + busbar_contingencies,
)
diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/powsybl/powsybl_backend.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/powsybl/powsybl_backend.py
index 321b964b7..cb51f37e3 100644
--- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/powsybl/powsybl_backend.py
+++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/powsybl/powsybl_backend.py
@@ -114,7 +114,7 @@ def __init__(
self.ac_p_values = net.get_branches(attributes=["p1"])["p1"]
dc_results = pp.loadflow.run_dc(net, lf_params)
- self.slack_id = dc_results[0].reference_bus_id
+ self.slack_id = net.get_extension("slackTerminal").iloc[0].bus_id
self.net = net
self.net_pu = get_network_as_pu(net)
@@ -642,6 +642,43 @@ def get_asset_topology(self) -> Optional[Topology]:
)
return None
+ def get_busbar_outage_map(self) -> Optional[dict[str, Sequence[str]]]:
+ """Get busbar outages grouped by asset-topology station id."""
+ mask_path = self._get_masks_path() / NETWORK_MASK_NAMES["busbar_for_nminus1"]
+ if not self.data_folder_dirfs.exists(str(mask_path)):
+ return None
+
+ busbar_sections = self.net.get_busbar_sections(attributes=["bus_id"])
+ busbar_for_nminus1 = load_numpy_filesystem(filesystem=self.data_folder_dirfs, file_path=str(mask_path))
+ selected_busbars = busbar_sections[busbar_for_nminus1]
+ selected_busbars = selected_busbars[selected_busbars["bus_id"].isin(self.get_node_ids())]
+
+ asset_topology = self.get_asset_topology()
+ busbar_to_station_id = {}
+ bus_id_to_station_id = {}
+ if asset_topology is not None:
+ busbar_to_station_id = {
+ busbar.grid_model_id: station.grid_model_id
+ for station in asset_topology.stations
+ for busbar in station.busbars
+ }
+ bus_id_to_station_id = {
+ busbar.bus_branch_bus_id: station.grid_model_id
+ for station in asset_topology.stations
+ for busbar in station.busbars
+ }
+
+ outage_map: dict[str, list[str]] = {}
+ for busbar_id, busbar in selected_busbars.iterrows():
+ station_id = busbar_to_station_id.get(str(busbar_id))
+ if station_id is None:
+ station_id = bus_id_to_station_id.get(str(busbar["bus_id"]))
+ if station_id is None:
+ continue
+ outage_map.setdefault(station_id, []).append(str(busbar_id))
+
+ return outage_map
+
def get_metadata(self) -> dict:
"""Get the path to the data_folder, masks_folder and the start datetime of the case"""
return {
diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess.py
index a1e6df3c3..16bee0db0 100644
--- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess.py
+++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess.py
@@ -83,6 +83,15 @@
logger = structlog.get_logger(__name__)
+def disable_busbar_outage_contingencies(network_data: NetworkData) -> NetworkData:
+ """Clear busbar-outage configuration from network data.
+
+ The importer may provide a busbar outage map unconditionally, but when preprocessing-side
+ busbar outages are disabled we must not reconstruct bus contingencies from it later on.
+ """
+ return replace(network_data, busbar_outage_map=None)
+
+
def compute_ptdf_if_not_given(network_data: NetworkData) -> NetworkData:
"""Compute the PTDF if not given.
@@ -1476,6 +1485,7 @@ def preprocess( # noqa: PLR0915
network_data = preprocess_bb_outages(network_data)
else:
logging_fn("preprocess_bb_outage", "BB-Outages disabled, skipping preprocessing step")
+ network_data = disable_busbar_outage_contingencies(network_data)
logging_fn("preprocess_done", None)
return network_data
diff --git a/packages/dc_solver_pkg/tests/conftest.py b/packages/dc_solver_pkg/tests/conftest.py
index a7b2b8c49..20323763a 100644
--- a/packages/dc_solver_pkg/tests/conftest.py
+++ b/packages/dc_solver_pkg/tests/conftest.py
@@ -48,6 +48,12 @@
oberrhein_data,
three_node_pst_example_folder_powsybl,
)
+from toop_engine_dc_solver.example_grids import (
+ grouped_pst_grid_example_data_folder as create_grouped_pst_grid_example_data_folder,
+)
+from toop_engine_dc_solver.example_grids import (
+ parallel_pst_data_folder as create_parallel_pst_data_folder,
+)
from toop_engine_dc_solver.jax.injections import (
convert_action_index_to_numpy,
random_injection,
@@ -729,6 +735,9 @@ def _test_grid_folder_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""Create a temporary folder with test grid node breaker data."""
tmp_path = tmp_path_factory.mktemp("test_grid_node_breaker")
node_breaker_folder_powsybl(tmp_path)
+ save_lf_params_to_fs(
+ SINGLE_SLACK, DirFileSystem(str(tmp_path)), Path(PREPROCESSING_PATHS["loadflow_parameters_file_path"])
+ )
return tmp_path
@@ -739,25 +748,14 @@ def test_grid_folder_path(_test_grid_folder_path: Path, tmp_path: Path) -> Path:
@pytest.fixture(scope="session")
-def network_data_test_grid(_test_grid_folder_path: Path, outage_map_test_grid: dict) -> NetworkData:
- class TestBackend(PowsyblBackend):
- def get_busbar_outage_map(self):
- return outage_map_test_grid
+def network_data_test_grid(_test_grid_folder_path: Path) -> NetworkData:
fs_dir = DirFileSystem(str(_test_grid_folder_path))
- backend = TestBackend(fs_dir, lf_params=SINGLE_SLACK)
+ backend = PowsyblBackend(fs_dir, lf_params=SINGLE_SLACK)
network_data = preprocess(backend, parameters=PreprocessParameters(preprocess_bb_outages=True))
return network_data
-@pytest.fixture(scope="session")
-def outage_map_test_grid():
- return {
- "VL2_0": ["BBS2_1", "BBS2_2", "BBS2_3"],
- "VL3_0": ["BBS3_1", "BBS3_2"],
- }
-
-
@pytest.fixture(scope="session")
def jax_inputs_test_grid(
network_data_test_grid: NetworkData,
@@ -1329,6 +1327,51 @@ def complex_grid_battery_hvdc_svc_3w_trafo_linear_0_1_data_folder(
return tmp_path
+@pytest.fixture(scope="session")
+def _parallel_pst_data_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ tmp_path = tmp_path_factory.mktemp("parallel_pst")
+ network_data = create_parallel_pst_data_folder(tmp_path)
+ save_network_data(tmp_path / "network_data.pkl", network_data)
+ return tmp_path
+
+
+@pytest.fixture(scope="function")
+def parallel_pst_data_folder(_parallel_pst_data_path: Path, tmp_path: Path) -> Path:
+ shutil.copytree(_parallel_pst_data_path, tmp_path, dirs_exist_ok=True)
+ return tmp_path
+
+
+@pytest.fixture(scope="session")
+def _grouped_pst_grid_example_data_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ tmp_path = tmp_path_factory.mktemp("grouped_pst")
+ network_data = create_grouped_pst_grid_example_data_folder(tmp_path)
+ save_network_data(tmp_path / "network_data.pkl", network_data)
+ return tmp_path
+
+
+@pytest.fixture(scope="function")
+def grouped_pst_grid_example_data_folder(_grouped_pst_grid_example_data_path: Path, tmp_path: Path) -> Path:
+ shutil.copytree(_grouped_pst_grid_example_data_path, tmp_path, dirs_exist_ok=True)
+ return tmp_path
+
+
+@pytest.fixture(scope="session")
+def _grouped_pst_grid_example_split_data_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ tmp_path = tmp_path_factory.mktemp("grouped_pst_split")
+ network_data = create_grouped_pst_grid_example_data_folder(tmp_path, split_group_station=True)
+ save_network_data(tmp_path / "network_data.pkl", network_data)
+ return tmp_path
+
+
+@pytest.fixture(scope="function")
+def grouped_pst_grid_example_split_data_folder(
+ _grouped_pst_grid_example_split_data_path: Path,
+ tmp_path: Path,
+) -> Path:
+ shutil.copytree(_grouped_pst_grid_example_split_data_path, tmp_path, dirs_exist_ok=True)
+ return tmp_path
+
+
@pytest.fixture(scope="function")
def three_node_pst_example_data_folder(tmp_path_factory: pytest.TempPathFactory) -> Path:
tmp_path = tmp_path_factory.mktemp("three_node_pst_example")
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set.json
index b9586fa08..9b21c9e80 100644
--- a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set.json
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set.json
@@ -1 +1,805 @@
-{"starting_topology":{"topology_id":"test_ucte_powsybl_example.xiidm","grid_model_file":"/workspaces/AICoE_HPC_RL_Grid_Model_Importing/data/Test_Grid_asset_topo/test_ucte_powsybl_example.xiidm","name":null,"stations":[{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":false,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[true,false,true,false,true,true],[false,true,false,true,false,false],[false,false,false,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL3_0","name":"VLevel3","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS3_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS3_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS3_3","type":"busbar","name":"bus3","int_id":2,"in_service":true},{"grid_model_id":"BBS3_4","type":"busbar","name":"bus4","int_id":3,"in_service":true}],"couplers":[{"grid_model_id":"VL3_BREAKER","type":"BREAKER","name":"VL3_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":false,"in_service":true,"asset_bay":null},{"grid_model_id":"VL3_BREAKER#0","type":"BREAKER","name":"VL3_BREAKER#0","busbar_from_id":0,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null},{"grid_model_id":"VL3_BREAKER#1","type":"BREAKER","name":"VL3_BREAKER#1","busbar_from_id":1,"busbar_to_id":3,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L3","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L32_BREAKER","sr_switch_foreign_id":{"BBS3_1":"L32_DISCONNECTOR_5_0","BBS3_2":"L32_DISCONNECTOR_5_1"}}},{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L62_BREAKER","sr_switch_foreign_id":{"BBS3_1":"L62_DISCONNECTOR_7_0","BBS3_2":"L62_DISCONNECTOR_7_1"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L72_BREAKER","sr_switch_foreign_id":{"BBS3_1":"L72_DISCONNECTOR_9_0","BBS3_2":"L72_DISCONNECTOR_9_1"}}},{"grid_model_id":"L9","type":"LINE","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L91_BREAKER","sr_switch_foreign_id":{"BBS3_1":"L91_DISCONNECTOR_11_0","BBS3_2":"L91_DISCONNECTOR_11_1"}}},{"grid_model_id":"load2","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load2_BREAKER","sr_switch_foreign_id":{"BBS3_1":"load2_DISCONNECTOR_19_0","BBS3_2":"load2_DISCONNECTOR_19_1"}}}],"asset_switching_table":[[true,true,false,true,false],[false,false,true,false,true],[false,false,false,false,false],[false,false,false,false,false]],"asset_connectivity":[[true,true,true,true,true],[true,true,true,true,true],[false,false,false,false,false],[false,false,false,false,false]],"model_log":[]}],"asset_setpoints":null,"timestamp":"2025-05-16T15:25:10.814573","metrics":null},"connectable_branches":[],"disconnectable_branches":[{"id":"L1","type":"LINE","kind":"branch"},{"id":"L2","type":"LINE","kind":"branch"},{"id":"L3","type":"LINE","kind":"branch"},{"id":"L4","type":"LINE","kind":"branch"},{"id":"L5","type":"LINE","kind":"branch"},{"id":"L6","type":"LINE","kind":"branch"},{"id":"L7","type":"LINE","kind":"branch"},{"id":"L8","type":"LINE","kind":"branch"}],"pst_ranges":[],"hvdc_ranges":[],"local_actions":[{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":false,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":true,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,false,false,false,false,true],[false,false,false,false,false,false],[true,true,true,true,true,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,true,false,true,true,true],[false,false,true,false,false,false],[true,false,false,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,true,true,false,false,true],[false,false,false,true,true,false],[true,false,false,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,false,false,true,true,true],[false,true,false,false,false,false],[true,false,true,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,true,true,false,true,true],[false,false,false,true,false,false],[true,false,false,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,true,false,true,false,true],[false,false,true,false,true,false],[true,false,false,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,false,true,false,true,true],[false,true,false,false,false,false],[true,false,false,true,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[true,true,false,false,true,true],[false,false,false,true,false,false],[false,false,true,false,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[false,true,false,false,true,true],[false,false,true,false,false,false],[true,false,false,true,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]},{"grid_model_id":"VL2_0","name":"VLevel2","type":null,"region":"BE","voltage_level":225.0,"busbars":[{"grid_model_id":"BBS2_1","type":"busbar","name":"bus1","int_id":0,"in_service":true},{"grid_model_id":"BBS2_2","type":"busbar","name":"bus2","int_id":1,"in_service":true},{"grid_model_id":"BBS2_3","type":"busbar","name":"bus3","int_id":2,"in_service":true}],"couplers":[{"grid_model_id":"VL2_BREAKER","type":"BREAKER","name":"VL2_BREAKER","busbar_from_id":0,"busbar_to_id":1,"open":true,"in_service":true,"asset_bay":null},{"grid_model_id":"VL2_BREAKER#0","type":"BREAKER","name":"VL2_BREAKER#0","busbar_from_id":1,"busbar_to_id":2,"open":false,"in_service":true,"asset_bay":null}],"assets":[{"grid_model_id":"L6","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L61_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L61_DISCONNECTOR_8_0","BBS2_2":"L61_DISCONNECTOR_8_1","BBS2_3":"L61_DISCONNECTOR_8_2"}}},{"grid_model_id":"L7","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L71_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L71_DISCONNECTOR_10_0","BBS2_2":"L71_DISCONNECTOR_10_1","BBS2_3":"L71_DISCONNECTOR_10_2"}}},{"grid_model_id":"L8","type":"LINE","name":"","in_service":true,"branch_end":"from","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L81_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L81_DISCONNECTOR_12_0","BBS2_2":"L81_DISCONNECTOR_12_1","BBS2_3":"L81_DISCONNECTOR_12_2"}}},{"grid_model_id":"L1","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L12_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L12_DISCONNECTOR_4_0","BBS2_2":"L12_DISCONNECTOR_4_1","BBS2_3":"L12_DISCONNECTOR_4_2"}}},{"grid_model_id":"L2","type":"LINE","name":"","in_service":true,"branch_end":"to","asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"L22_BREAKER","sr_switch_foreign_id":{"BBS2_1":"L22_DISCONNECTOR_6_0","BBS2_2":"L22_DISCONNECTOR_6_1","BBS2_3":"L22_DISCONNECTOR_6_2"}}},{"grid_model_id":"load1","type":"LOAD","name":"","in_service":true,"branch_end":null,"asset_bay":{"sl_switch_foreign_id":null,"dv_switch_foreign_id":"load1_BREAKER","sr_switch_foreign_id":{"BBS2_1":"load1_DISCONNECTOR_18_0","BBS2_2":"load1_DISCONNECTOR_18_1","BBS2_3":"load1_DISCONNECTOR_18_2"}}}],"asset_switching_table":[[true,false,false,false,true,true],[false,true,false,false,false,false],[false,false,true,true,false,false]],"asset_connectivity":[[true,true,true,true,true,true],[true,true,true,true,true,true],[true,true,true,true,true,true]],"model_log":[]}],"global_actions":[]}
+{
+ "starting_topology": {
+ "topology_id": "grid.xiidm",
+ "grid_model_file": "grid.xiidm",
+ "name": null,
+ "stations": [
+ {
+ "grid_model_id": "VL2_0",
+ "name": "VLevel2",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS2_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ },
+ {
+ "grid_model_id": "BBS2_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ },
+ {
+ "grid_model_id": "BBS2_3",
+ "type": "busbar",
+ "name": "bus3",
+ "int_id": 2,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL2_BREAKER",
+ "type": "BREAKER",
+ "name": "VL2_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ },
+ {
+ "grid_model_id": "VL2_BREAKER#0",
+ "type": "BREAKER",
+ "name": "VL2_BREAKER#0",
+ "busbar_from_id": 1,
+ "busbar_to_id": 2,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L1",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L12_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L12_DISCONNECTOR_4_0",
+ "BBS2_2": "L12_DISCONNECTOR_4_1",
+ "BBS2_3": "L12_DISCONNECTOR_4_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L2",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L22_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L22_DISCONNECTOR_6_0",
+ "BBS2_2": "L22_DISCONNECTOR_6_1",
+ "BBS2_3": "L22_DISCONNECTOR_6_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L6",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L61_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L61_DISCONNECTOR_8_0",
+ "BBS2_2": "L61_DISCONNECTOR_8_1",
+ "BBS2_3": "L61_DISCONNECTOR_8_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L7",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L71_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L71_DISCONNECTOR_10_0",
+ "BBS2_2": "L71_DISCONNECTOR_10_1",
+ "BBS2_3": "L71_DISCONNECTOR_10_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L8",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L81_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L81_DISCONNECTOR_12_0",
+ "BBS2_2": "L81_DISCONNECTOR_12_1",
+ "BBS2_3": "L81_DISCONNECTOR_12_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "load1",
+ "type": "LOAD",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "load1_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "load1_DISCONNECTOR_18_0",
+ "BBS2_2": "load1_DISCONNECTOR_18_1",
+ "BBS2_3": "load1_DISCONNECTOR_18_2"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ false,
+ true,
+ false,
+ true,
+ true
+ ],
+ [
+ false,
+ true,
+ false,
+ true,
+ false,
+ false
+ ],
+ [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ },
+ {
+ "grid_model_id": "VL3_0",
+ "name": "VLevel3",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS3_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL3_0"
+ },
+ {
+ "grid_model_id": "BBS3_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL3_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL3_BREAKER",
+ "type": "BREAKER",
+ "name": "VL3_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L3",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L32_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L32_DISCONNECTOR_3_0",
+ "BBS3_2": "L32_DISCONNECTOR_3_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L6",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L62_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L62_DISCONNECTOR_5_0",
+ "BBS3_2": "L62_DISCONNECTOR_5_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L7",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L72_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L72_DISCONNECTOR_7_0",
+ "BBS3_2": "L72_DISCONNECTOR_7_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L9",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L91_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L91_DISCONNECTOR_9_0",
+ "BBS3_2": "L91_DISCONNECTOR_9_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "load2",
+ "type": "LOAD",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "load2_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "load2_DISCONNECTOR_13_0",
+ "BBS3_2": "load2_DISCONNECTOR_13_1"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ true,
+ false,
+ true,
+ false
+ ],
+ [
+ false,
+ false,
+ true,
+ false,
+ true
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ },
+ {
+ "grid_model_id": "VL4_0",
+ "name": "VLevel4",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS4_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL4_0"
+ },
+ {
+ "grid_model_id": "BBS4_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL4_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL4_BREAKER",
+ "type": "BREAKER",
+ "name": "VL4_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L4",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L42_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS4_1": "L42_DISCONNECTOR_3_0",
+ "BBS4_2": "L42_DISCONNECTOR_3_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L5",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L52_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS4_1": "L52_DISCONNECTOR_5_0",
+ "BBS4_2": "L52_DISCONNECTOR_5_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L8",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L82_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS4_1": "L82_DISCONNECTOR_7_0",
+ "BBS4_2": "L82_DISCONNECTOR_7_1"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ false,
+ true
+ ],
+ [
+ false,
+ true,
+ false
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ },
+ {
+ "grid_model_id": "VL5_0",
+ "name": "VLevel5",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS5_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL5_0"
+ }
+ ],
+ "couplers": [],
+ "assets": [
+ {
+ "grid_model_id": "L9",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L92_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS5_1": "L92_DISCONNECTOR_2_0"
+ }
+ }
+ },
+ {
+ "grid_model_id": "generator3",
+ "type": "GENERATOR",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "generator3_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS5_1": "generator3_DISCONNECTOR_4_0"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ true
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ }
+ ],
+ "asset_setpoints": null,
+ "timestamp": "2026-07-02T10:35:24.879394",
+ "metrics": null
+ },
+ "simplified_starting_topology": {
+ "topology_id": "grid.xiidm",
+ "grid_model_file": "grid.xiidm",
+ "name": null,
+ "stations": [
+ {
+ "grid_model_id": "VL2_0",
+ "name": "VLevel2",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS2_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ },
+ {
+ "grid_model_id": "BBS2_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ },
+ {
+ "grid_model_id": "BBS2_3",
+ "type": "busbar",
+ "name": "bus3",
+ "int_id": 2,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL2_BREAKER",
+ "type": "BREAKER",
+ "name": "VL2_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ },
+ {
+ "grid_model_id": "VL2_BREAKER#0",
+ "type": "BREAKER",
+ "name": "VL2_BREAKER#0",
+ "busbar_from_id": 1,
+ "busbar_to_id": 2,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L6",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L61_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L61_DISCONNECTOR_8_0",
+ "BBS2_2": "L61_DISCONNECTOR_8_1",
+ "BBS2_3": "L61_DISCONNECTOR_8_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L7",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L71_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L71_DISCONNECTOR_10_0",
+ "BBS2_2": "L71_DISCONNECTOR_10_1",
+ "BBS2_3": "L71_DISCONNECTOR_10_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L8",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "from",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L81_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L81_DISCONNECTOR_12_0",
+ "BBS2_2": "L81_DISCONNECTOR_12_1",
+ "BBS2_3": "L81_DISCONNECTOR_12_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L1",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L12_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L12_DISCONNECTOR_4_0",
+ "BBS2_2": "L12_DISCONNECTOR_4_1",
+ "BBS2_3": "L12_DISCONNECTOR_4_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L2",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": "to",
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L22_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L22_DISCONNECTOR_6_0",
+ "BBS2_2": "L22_DISCONNECTOR_6_1",
+ "BBS2_3": "L22_DISCONNECTOR_6_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "load1",
+ "type": "LOAD",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "load1_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "load1_DISCONNECTOR_18_0",
+ "BBS2_2": "load1_DISCONNECTOR_18_1",
+ "BBS2_3": "load1_DISCONNECTOR_18_2"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ false,
+ true,
+ true,
+ false,
+ true
+ ],
+ [
+ false,
+ true,
+ false,
+ false,
+ true,
+ false
+ ],
+ [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ }
+ ],
+ "asset_setpoints": null,
+ "timestamp": "2026-07-02T10:35:24.879394",
+ "metrics": null
+ },
+ "connectable_branches": [],
+ "disconnectable_branches": [
+ {
+ "id": "L1",
+ "name": "VL1_3 ## VL2_3 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L2",
+ "name": "VL1_5 ## VL2_5 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L3",
+ "name": "VL1_7 ## VL3_2 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L4",
+ "name": "VL1_9 ## VL4_2 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L5",
+ "name": "VL1_11 ## VL4_4 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L6",
+ "name": "VL2_7 ## VL3_4 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L7",
+ "name": "VL2_9 ## VL3_6 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ },
+ {
+ "id": "L8",
+ "name": "VL2_11 ## VL4_6 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "pst_ranges": [],
+ "hvdc_ranges": [],
+ "local_actions": []
+}
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set_diffs.hdf5 b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set_diffs.hdf5
new file mode 100644
index 000000000..e47fda4b2
Binary files /dev/null and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set_diffs.hdf5 differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/grid.xiidm b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/grid.xiidm
index 6e7c7ea43..b5565c5f5 100644
--- a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/grid.xiidm
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/grid.xiidm
@@ -1,359 +1,246 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/importer_auxiliary_data.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/importer_auxiliary_data.json
index 7085a4902..3d9d2fde4 100644
--- a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/importer_auxiliary_data.json
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/importer_auxiliary_data.json
@@ -1,260 +1,215 @@
-{
- "id_lists": {
- "white_list": [],
- "black_list": [],
- "relevant_subs": [
- "VL2_0",
- "VL3_0"
- ],
- "line_for_nminus1": [
- "L1",
- "L2",
- "L3",
- "L4",
- "L5",
- "L6",
- "L7",
- "L8",
- "L9",
- "L10"
- ],
- "trafo_for_nminus1": [],
- "tie_line_for_nminus1": [],
- "boundary_line_for_nminus1": [],
- "generator_for_nminus1": [
- "generator1",
- "generator2",
- "generator3"
- ],
- "load_for_nminus1": [
- "load1",
- "load2",
- "load6"
- ],
- "switch_for_nminus1": [
- "L11_BREAKER",
- "L11_DISCONNECTOR_3_0",
- "L11_DISCONNECTOR_3_1",
- "L21_BREAKER",
- "L21_DISCONNECTOR_5_0",
- "L21_DISCONNECTOR_5_1",
- "L31_BREAKER",
- "L31_DISCONNECTOR_7_0",
- "L31_DISCONNECTOR_7_1",
- "L41_BREAKER",
- "L41_DISCONNECTOR_9_0",
- "L41_DISCONNECTOR_9_1",
- "L51_BREAKER",
- "L51_DISCONNECTOR_11_0",
- "L51_DISCONNECTOR_11_1",
- "VL1_BREAKER",
- "VL1_DISCONNECTOR_12_0",
- "VL1_DISCONNECTOR_13_1",
- "generator1_BREAKER",
- "generator1_DISCONNECTOR_15_0",
- "generator1_DISCONNECTOR_15_1",
- "generator2_BREAKER",
- "generator2_DISCONNECTOR_17_0",
- "generator2_DISCONNECTOR_17_1",
- "L12_BREAKER",
- "L12_DISCONNECTOR_4_0",
- "L12_DISCONNECTOR_4_1",
- "L12_DISCONNECTOR_4_2",
- "L22_BREAKER",
- "L22_DISCONNECTOR_6_0",
- "L22_DISCONNECTOR_6_1",
- "L22_DISCONNECTOR_6_2",
- "L61_BREAKER",
- "L61_DISCONNECTOR_8_0",
- "L61_DISCONNECTOR_8_1",
- "L61_DISCONNECTOR_8_2",
- "L71_BREAKER",
- "L71_DISCONNECTOR_10_0",
- "L71_DISCONNECTOR_10_1",
- "L71_DISCONNECTOR_10_2",
- "L81_BREAKER",
- "L81_DISCONNECTOR_12_0",
- "L81_DISCONNECTOR_12_1",
- "L81_DISCONNECTOR_12_2",
- "VL2_BREAKER",
- "VL2_DISCONNECTOR_13_0",
- "VL2_DISCONNECTOR_13_1",
- "VL2_DISCONNECTOR_14_1",
- "VL2_DISCONNECTOR_14_2",
- "VL2_BREAKER#0",
- "VL2_DISCONNECTOR_15_0",
- "VL2_DISCONNECTOR_15_1",
- "VL2_DISCONNECTOR_16_1",
- "VL2_DISCONNECTOR_16_2",
- "load1_BREAKER",
- "load1_DISCONNECTOR_18_0",
- "load1_DISCONNECTOR_18_1",
- "load1_DISCONNECTOR_18_2",
- "L32_BREAKER",
- "L32_DISCONNECTOR_5_0",
- "L32_DISCONNECTOR_5_1",
- "L62_BREAKER",
- "L62_DISCONNECTOR_7_0",
- "L62_DISCONNECTOR_7_1",
- "L72_BREAKER",
- "L72_DISCONNECTOR_9_0",
- "L72_DISCONNECTOR_9_1",
- "L91_BREAKER",
- "L91_DISCONNECTOR_11_0",
- "L91_DISCONNECTOR_11_1",
- "VL3_BREAKER",
- "VL3_DISCONNECTOR_12_0",
- "VL3_DISCONNECTOR_13_1",
- "VL3_BREAKER#0",
- "VL3_DISCONNECTOR_14_0",
- "VL3_DISCONNECTOR_14_1",
- "VL3_DISCONNECTOR_15_2",
- "VL3_DISCONNECTOR_15_3",
- "VL3_BREAKER#1",
- "VL3_DISCONNECTOR_16_0",
- "VL3_DISCONNECTOR_16_1",
- "VL3_DISCONNECTOR_17_2",
- "VL3_DISCONNECTOR_17_3",
- "load2_BREAKER",
- "load2_DISCONNECTOR_19_0",
- "load2_DISCONNECTOR_19_1",
- "L42_BREAKER",
- "L42_DISCONNECTOR_5_0",
- "L42_DISCONNECTOR_5_1",
- "L42_DISCONNECTOR_5_2",
- "L42_DISCONNECTOR_5_3",
- "L52_BREAKER",
- "L52_DISCONNECTOR_7_0",
- "L52_DISCONNECTOR_7_1",
- "L52_DISCONNECTOR_7_2",
- "L52_DISCONNECTOR_7_3",
- "L82_BREAKER",
- "L82_DISCONNECTOR_9_0",
- "L82_DISCONNECTOR_9_1",
- "L82_DISCONNECTOR_9_2",
- "L82_DISCONNECTOR_9_3",
- "VL4_BREAKER",
- "VL4_DISCONNECTOR_10_0",
- "VL4_DISCONNECTOR_10_1",
- "VL4_DISCONNECTOR_10_2",
- "VL4_DISCONNECTOR_11_1",
- "VL4_DISCONNECTOR_11_2",
- "VL4_DISCONNECTOR_11_3",
- "VL4_BREAKER#0",
- "VL4_DISCONNECTOR_12_0",
- "VL4_DISCONNECTOR_12_1",
- "VL4_DISCONNECTOR_12_2",
- "VL4_DISCONNECTOR_13_1",
- "VL4_DISCONNECTOR_13_2",
- "VL4_DISCONNECTOR_13_3",
- "VL4_BREAKER#1",
- "VL4_DISCONNECTOR_14_0",
- "VL4_DISCONNECTOR_14_1",
- "VL4_DISCONNECTOR_14_2",
- "VL4_DISCONNECTOR_15_1",
- "VL4_DISCONNECTOR_15_2",
- "VL4_DISCONNECTOR_15_3",
- "VL5_BREAKER",
- "VL5_DISCONNECTOR_3_0",
- "VL5_DISCONNECTOR_4_1",
- "VL5_BREAKER#0",
- "VL5_DISCONNECTOR_5_0",
- "VL5_DISCONNECTOR_6_2",
- "L92_BREAKER",
- "L92_DISCONNECTOR_8_0",
- "L92_DISCONNECTOR_8_1",
- "L92_DISCONNECTOR_8_2",
- "L101_BREAKER",
- "L101_DISCONNECTOR_10_0",
- "L101_DISCONNECTOR_10_1",
- "L101_DISCONNECTOR_10_2",
- "generator3_BREAKER",
- "generator3_DISCONNECTOR_12_0",
- "generator3_DISCONNECTOR_12_1",
- "generator3_DISCONNECTOR_12_2",
- "VL6_DISCONNECTOR_0_6",
- "VL6_BREAKER_1_1",
- "VL6_DISCONNECTOR_7_2",
- "VL6_DISCONNECTOR_1_8",
- "VL6_BREAKER_2_1",
- "VL6_DISCONNECTOR_9_3",
- "VL6_DISCONNECTOR_2_4",
- "VL6_DISCONNECTOR_3_5",
- "VL6_BREAKER",
- "VL6_DISCONNECTOR_10_2",
- "VL6_DISCONNECTOR_11_3",
- "load6_BREAKER",
- "load6_DISCONNECTOR_13_0",
- "load6_DISCONNECTOR_13_1",
- "L102_BREAKER",
- "L102_DISCONNECTOR_15_0",
- "L102_DISCONNECTOR_15_1"
- ],
- "line_disconnectable": [
- "L1",
- "L2",
- "L3",
- "L4",
- "L5",
- "L6",
- "L7",
- "L8",
- "L9",
- "L10"
- ]
- },
- "import_result": {
- "data_folder": "/workspaces/AICoE_HPC_RL_Grid_Model_Importing/notebooks/../data/20240113/pst-export/test_ucte_powsybl_example",
- "n_relevant_subs": 2,
- "n_low_impedance_lines": 0,
- "n_branch_across_switch": 0,
- "n_line_for_nminus1": 10,
- "n_line_for_reward": 3,
- "n_line_disconnectable": 10,
- "n_trafo_for_nminus1": 0,
- "n_trafo_for_reward": 0,
- "n_trafo_disconnectable": 0,
- "n_tie_line_for_reward": 0,
- "n_tie_line_for_nminus1": 0,
- "n_tie_line_disconnectable": 0,
- "n_boundary_line_for_nminus1": 0,
- "n_generator_for_nminus1": 3,
- "n_load_for_nminus1": 3,
- "n_switch_for_nminus1": 157,
- "n_switch_for_reward": 0,
- "n_white_list": 0,
- "n_white_list_applied": 0,
- "n_black_list": 0,
- "n_black_list_applied": 0,
- "grid_type": "ucte"
- },
- "border_current": {},
- "network_changes": {},
- "import_parameter": {
- "area_settings": {
- "control_area": [
- "BE"
- ],
- "view_area": [
- "BE"
- ],
- "nminus1_area": [
- "BE"
- ],
- "cutoff_voltage": 150,
- "cross_border_limits_n0": null,
- "cross_border_limits_n1": null,
- "dso_trafo_factors": null,
- "dso_trafo_weight": 1.0,
- "border_line_factors": null,
- "border_line_weight": 1.0
- },
- "data_folder": "/workspaces/AICoE_HPC_RL_Grid_Model_Importing/notebooks/../data/20240113/pst-export/test_ucte_powsybl_example",
- "grid_model_file": "/workspaces/AICoE_HPC_RL_Grid_Model_Importing/data/Test_Grid_asset_topo/test_ucte_powsybl_example.xiidm",
- "data_type": "cgmes",
- "white_list_file": "/workspaces/AICoE_HPC_RL_Grid_Model_Importing/notebooks/../data/20240113/BlackWhiteListen/CB_White-Liste.csv",
- "black_list_file": "/workspaces/AICoE_HPC_RL_Grid_Model_Importing/notebooks/../data/20240113/BlackWhiteListen/CB_Black-Liste.csv",
- "ignore_list_file": null,
- "contingency_list_file": null
- }
-}
+{
+ "id_lists": {
+ "white_list": [],
+ "black_list": [],
+ "relevant_subs": [
+ "VL2_0",
+ "VL3_0"
+ ],
+ "line_for_nminus1": [
+ "L1",
+ "L2",
+ "L3",
+ "L4",
+ "L5",
+ "L6",
+ "L7",
+ "L8",
+ "L9"
+ ],
+ "trafo_for_nminus1": [],
+ "tie_line_for_nminus1": [],
+ "boundary_line_for_nminus1": [],
+ "generator_for_nminus1": [
+ "generator1",
+ "generator2",
+ "generator3"
+ ],
+ "load_for_nminus1": [
+ "load1",
+ "load2"
+ ],
+ "switch_for_nminus1": [
+ "L11_BREAKER",
+ "L11_DISCONNECTOR_4_0",
+ "L11_DISCONNECTOR_4_1",
+ "L11_DISCONNECTOR_4_2",
+ "L21_BREAKER",
+ "L21_DISCONNECTOR_6_0",
+ "L21_DISCONNECTOR_6_1",
+ "L21_DISCONNECTOR_6_2",
+ "L31_BREAKER",
+ "L31_DISCONNECTOR_8_0",
+ "L31_DISCONNECTOR_8_1",
+ "L31_DISCONNECTOR_8_2",
+ "L41_BREAKER",
+ "L41_DISCONNECTOR_10_0",
+ "L41_DISCONNECTOR_10_1",
+ "L41_DISCONNECTOR_10_2",
+ "L51_BREAKER",
+ "L51_DISCONNECTOR_12_0",
+ "L51_DISCONNECTOR_12_1",
+ "L51_DISCONNECTOR_12_2",
+ "VL1_BREAKER",
+ "VL1_DISCONNECTOR_13_0",
+ "VL1_DISCONNECTOR_13_1",
+ "VL1_DISCONNECTOR_14_1",
+ "VL1_DISCONNECTOR_14_2",
+ "VL1_BREAKER#0",
+ "VL1_DISCONNECTOR_15_0",
+ "VL1_DISCONNECTOR_15_1",
+ "VL1_DISCONNECTOR_16_1",
+ "VL1_DISCONNECTOR_16_2",
+ "generator1_BREAKER",
+ "generator1_DISCONNECTOR_18_0",
+ "generator1_DISCONNECTOR_18_1",
+ "generator1_DISCONNECTOR_18_2",
+ "generator2_BREAKER",
+ "generator2_DISCONNECTOR_20_0",
+ "generator2_DISCONNECTOR_20_1",
+ "generator2_DISCONNECTOR_20_2",
+ "L12_BREAKER",
+ "L12_DISCONNECTOR_4_0",
+ "L12_DISCONNECTOR_4_1",
+ "L12_DISCONNECTOR_4_2",
+ "L22_BREAKER",
+ "L22_DISCONNECTOR_6_0",
+ "L22_DISCONNECTOR_6_1",
+ "L22_DISCONNECTOR_6_2",
+ "L61_BREAKER",
+ "L61_DISCONNECTOR_8_0",
+ "L61_DISCONNECTOR_8_1",
+ "L61_DISCONNECTOR_8_2",
+ "L71_BREAKER",
+ "L71_DISCONNECTOR_10_0",
+ "L71_DISCONNECTOR_10_1",
+ "L71_DISCONNECTOR_10_2",
+ "L81_BREAKER",
+ "L81_DISCONNECTOR_12_0",
+ "L81_DISCONNECTOR_12_1",
+ "L81_DISCONNECTOR_12_2",
+ "VL2_BREAKER",
+ "VL2_DISCONNECTOR_13_0",
+ "VL2_DISCONNECTOR_13_1",
+ "VL2_DISCONNECTOR_14_1",
+ "VL2_DISCONNECTOR_14_2",
+ "VL2_BREAKER#0",
+ "VL2_DISCONNECTOR_15_0",
+ "VL2_DISCONNECTOR_15_1",
+ "VL2_DISCONNECTOR_16_1",
+ "VL2_DISCONNECTOR_16_2",
+ "load1_BREAKER",
+ "load1_DISCONNECTOR_18_0",
+ "load1_DISCONNECTOR_18_1",
+ "load1_DISCONNECTOR_18_2",
+ "L32_BREAKER",
+ "L32_DISCONNECTOR_3_0",
+ "L32_DISCONNECTOR_3_1",
+ "L62_BREAKER",
+ "L62_DISCONNECTOR_5_0",
+ "L62_DISCONNECTOR_5_1",
+ "L72_BREAKER",
+ "L72_DISCONNECTOR_7_0",
+ "L72_DISCONNECTOR_7_1",
+ "L91_BREAKER",
+ "L91_DISCONNECTOR_9_0",
+ "L91_DISCONNECTOR_9_1",
+ "VL3_BREAKER",
+ "VL3_DISCONNECTOR_10_0",
+ "VL3_DISCONNECTOR_11_1",
+ "load2_BREAKER",
+ "load2_DISCONNECTOR_13_0",
+ "load2_DISCONNECTOR_13_1",
+ "L42_BREAKER",
+ "L42_DISCONNECTOR_3_0",
+ "L42_DISCONNECTOR_3_1",
+ "L52_BREAKER",
+ "L52_DISCONNECTOR_5_0",
+ "L52_DISCONNECTOR_5_1",
+ "L82_BREAKER",
+ "L82_DISCONNECTOR_7_0",
+ "L82_DISCONNECTOR_7_1",
+ "VL4_BREAKER",
+ "VL4_DISCONNECTOR_8_0",
+ "VL4_DISCONNECTOR_9_1",
+ "L92_BREAKER",
+ "L92_DISCONNECTOR_2_0",
+ "generator3_BREAKER",
+ "generator3_DISCONNECTOR_4_0"
+ ],
+ "line_disconnectable": [
+ "L1",
+ "L2",
+ "L3",
+ "L4",
+ "L5",
+ "L6",
+ "L7",
+ "L8",
+ "L9"
+ ],
+ "trafo_disconnectable": []
+ },
+ "import_result": {
+ "data_folder": "../data/grid_node_breaker/grid",
+ "n_relevant_subs": 2,
+ "n_low_impedance_lines": 0,
+ "n_branch_across_switch": 0,
+ "n_line_for_nminus1": 9,
+ "n_line_for_reward": 3,
+ "n_line_disconnectable": 9,
+ "n_trafo_for_nminus1": 0,
+ "n_trafo_for_reward": 0,
+ "n_trafo_disconnectable": 0,
+ "n_tie_line_for_reward": 0,
+ "n_tie_line_for_nminus1": 0,
+ "n_tie_line_disconnectable": 0,
+ "n_boundary_line_for_nminus1": 0,
+ "n_generator_for_nminus1": 3,
+ "n_load_for_nminus1": 2,
+ "n_switch_for_nminus1": 106,
+ "n_switch_for_reward": 0,
+ "n_white_list": 0,
+ "n_white_list_applied": 0,
+ "n_black_list": 0,
+ "n_black_list_applied": 0,
+ "grid_type": "cgmes"
+ },
+ "border_current": {},
+ "network_changes": {},
+ "import_parameter": {
+ "area_settings": {
+ "control_area": [
+ ""
+ ],
+ "view_area": [
+ ""
+ ],
+ "nminus1_area": [
+ ""
+ ],
+ "cutoff_voltage": 10,
+ "dso_trafo_factors": null,
+ "dso_trafo_weight": 1.0,
+ "border_line_factors": null,
+ "border_line_weight": 1.0
+ },
+ "fail_on_non_convergence": true,
+ "data_folder": "../data/grid_node_breaker/grid",
+ "grid_model_file": "../data/grid_node_breaker/grid.xiidm",
+ "data_type": "cgmes",
+ "white_list_file": null,
+ "black_list_file": null,
+ "ignore_list_file": null,
+ "select_by_voltage_level_id_list": null,
+ "ingress_id": null,
+ "contingency_list_file": null,
+ "schema_format": null,
+ "relevant_station_rules": {
+ "min_busbars": 2,
+ "min_connected_branches": 4,
+ "min_connected_elements": 4
+ },
+ "loadflow_parameters_file": null
+ }
+}
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/asset_topology.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/asset_topology.json
index 1083d710e..d0566e0fd 100644
--- a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/asset_topology.json
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/asset_topology.json
@@ -1,420 +1,533 @@
-{
- "topology_id": "test_ucte_powsybl_example.xiidm",
- "grid_model_file": "/workspaces/AICoE_HPC_RL_Grid_Model_Importing/data/Test_Grid_asset_topo/test_ucte_powsybl_example.xiidm",
- "name": null,
- "stations": [
- {
- "grid_model_id": "VL2_0",
- "name": "VLevel2",
- "type": null,
- "region": "BE",
- "voltage_level": 225.0,
- "busbars": [
- {
- "grid_model_id": "BBS2_1",
- "type": "busbar",
- "name": "bus1",
- "int_id": 0,
- "in_service": true
- },
- {
- "grid_model_id": "BBS2_2",
- "type": "busbar",
- "name": "bus2",
- "int_id": 1,
- "in_service": true
- },
- {
- "grid_model_id": "BBS2_3",
- "type": "busbar",
- "name": "bus3",
- "int_id": 2,
- "in_service": true
- }
- ],
- "couplers": [
- {
- "grid_model_id": "VL2_BREAKER",
- "type": "BREAKER",
- "name": "VL2_BREAKER",
- "busbar_from_id": 0,
- "busbar_to_id": 1,
- "open": false,
- "in_service": true,
- "asset_bay": null
- },
- {
- "grid_model_id": "VL2_BREAKER#0",
- "type": "BREAKER",
- "name": "VL2_BREAKER#0",
- "busbar_from_id": 1,
- "busbar_to_id": 2,
- "open": false,
- "in_service": true,
- "asset_bay": null
- }
- ],
- "assets": [
- {
- "grid_model_id": "L1",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L12_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS2_1": "L12_DISCONNECTOR_4_0",
- "BBS2_2": "L12_DISCONNECTOR_4_1",
- "BBS2_3": "L12_DISCONNECTOR_4_2"
- }
- }
- },
- {
- "grid_model_id": "L2",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L22_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS2_1": "L22_DISCONNECTOR_6_0",
- "BBS2_2": "L22_DISCONNECTOR_6_1",
- "BBS2_3": "L22_DISCONNECTOR_6_2"
- }
- }
- },
- {
- "grid_model_id": "L6",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L61_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS2_1": "L61_DISCONNECTOR_8_0",
- "BBS2_2": "L61_DISCONNECTOR_8_1",
- "BBS2_3": "L61_DISCONNECTOR_8_2"
- }
- }
- },
- {
- "grid_model_id": "L7",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L71_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS2_1": "L71_DISCONNECTOR_10_0",
- "BBS2_2": "L71_DISCONNECTOR_10_1",
- "BBS2_3": "L71_DISCONNECTOR_10_2"
- }
- }
- },
- {
- "grid_model_id": "L8",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L81_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS2_1": "L81_DISCONNECTOR_12_0",
- "BBS2_2": "L81_DISCONNECTOR_12_1",
- "BBS2_3": "L81_DISCONNECTOR_12_2"
- }
- }
- },
- {
- "grid_model_id": "load1",
- "type": "LOAD",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "load1_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS2_1": "load1_DISCONNECTOR_18_0",
- "BBS2_2": "load1_DISCONNECTOR_18_1",
- "BBS2_3": "load1_DISCONNECTOR_18_2"
- }
- }
- }
- ],
- "asset_switching_table": [
- [
- true,
- false,
- true,
- false,
- true,
- true
- ],
- [
- false,
- true,
- false,
- true,
- false,
- false
- ],
- [
- false,
- false,
- false,
- false,
- false,
- false
- ]
- ],
- "asset_connectivity": [
- [
- true,
- true,
- true,
- true,
- true,
- true
- ],
- [
- true,
- true,
- true,
- true,
- true,
- true
- ],
- [
- true,
- true,
- true,
- true,
- true,
- true
- ]
- ],
- "model_log": []
- },
- {
- "grid_model_id": "VL3_0",
- "name": "VLevel3",
- "type": null,
- "region": "BE",
- "voltage_level": 225.0,
- "busbars": [
- {
- "grid_model_id": "BBS3_1",
- "type": "busbar",
- "name": "bus1",
- "int_id": 0,
- "in_service": true
- },
- {
- "grid_model_id": "BBS3_2",
- "type": "busbar",
- "name": "bus2",
- "int_id": 1,
- "in_service": true
- },
- {
- "grid_model_id": "BBS3_3",
- "type": "busbar",
- "name": "bus3",
- "int_id": 2,
- "in_service": true
- },
- {
- "grid_model_id": "BBS3_4",
- "type": "busbar",
- "name": "bus4",
- "int_id": 3,
- "in_service": true
- }
- ],
- "couplers": [
- {
- "grid_model_id": "VL3_BREAKER",
- "type": "BREAKER",
- "name": "VL3_BREAKER",
- "busbar_from_id": 0,
- "busbar_to_id": 1,
- "open": false,
- "in_service": true,
- "asset_bay": null
- },
- {
- "grid_model_id": "VL3_BREAKER#0",
- "type": "BREAKER",
- "name": "VL3_BREAKER#0",
- "busbar_from_id": 0,
- "busbar_to_id": 2,
- "open": false,
- "in_service": true,
- "asset_bay": null
- },
- {
- "grid_model_id": "VL3_BREAKER#1",
- "type": "BREAKER",
- "name": "VL3_BREAKER#1",
- "busbar_from_id": 1,
- "busbar_to_id": 3,
- "open": false,
- "in_service": true,
- "asset_bay": null
- }
- ],
- "assets": [
- {
- "grid_model_id": "L3",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L32_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS3_1": "L32_DISCONNECTOR_3_0",
- "BBS3_2": "L32_DISCONNECTOR_3_1"
- }
- }
- },
- {
- "grid_model_id": "L6",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L62_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS3_1": "L62_DISCONNECTOR_5_0",
- "BBS3_2": "L62_DISCONNECTOR_5_1"
- }
- }
- },
- {
- "grid_model_id": "L7",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L72_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS3_1": "L72_DISCONNECTOR_7_0",
- "BBS3_2": "L72_DISCONNECTOR_7_1"
- }
- }
- },
- {
- "grid_model_id": "L9",
- "type": "LINE",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "L91_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS3_1": "L91_DISCONNECTOR_9_0",
- "BBS3_2": "L91_DISCONNECTOR_9_1"
- }
- }
- },
- {
- "grid_model_id": "load2",
- "type": "LOAD",
- "name": "",
- "in_service": true,
- "branch_end": null,
- "asset_bay": {
- "sl_switch_grid_model_id": null,
- "dv_switch_grid_model_id": "load2_BREAKER",
- "sr_switch_grid_model_id": {
- "BBS3_1": "load2_DISCONNECTOR_13_0",
- "BBS3_2": "load2_DISCONNECTOR_13_1"
- }
- }
- }
- ],
- "asset_switching_table": [
- [
- true,
- true,
- false,
- true,
- false
- ],
- [
- false,
- false,
- true,
- false,
- true
- ],
- [
- false,
- false,
- false,
- false,
- false
- ],
- [
- false,
- false,
- false,
- false,
- false
- ]
- ],
- "asset_connectivity": [
- [
- true,
- true,
- true,
- true,
- true
- ],
- [
- true,
- true,
- true,
- true,
- true
- ],
- [
- false,
- false,
- false,
- false,
- false
- ],
- [
- false,
- false,
- false,
- false,
- false
- ]
- ],
- "model_log": []
- }
- ],
- "asset_setpoints": null,
- "timestamp": "2025-05-16T15:25:10.814573",
- "metrics": null
-}
+{
+ "topology_id": "grid.xiidm",
+ "grid_model_file": "grid.xiidm",
+ "name": null,
+ "stations": [
+ {
+ "grid_model_id": "VL2_0",
+ "name": "VLevel2",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS2_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ },
+ {
+ "grid_model_id": "BBS2_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ },
+ {
+ "grid_model_id": "BBS2_3",
+ "type": "busbar",
+ "name": "bus3",
+ "int_id": 2,
+ "in_service": true,
+ "bus_branch_bus_id": "VL2_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL2_BREAKER",
+ "type": "BREAKER",
+ "name": "VL2_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ },
+ {
+ "grid_model_id": "VL2_BREAKER#0",
+ "type": "BREAKER",
+ "name": "VL2_BREAKER#0",
+ "busbar_from_id": 1,
+ "busbar_to_id": 2,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L1",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L12_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L12_DISCONNECTOR_4_0",
+ "BBS2_2": "L12_DISCONNECTOR_4_1",
+ "BBS2_3": "L12_DISCONNECTOR_4_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L2",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L22_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L22_DISCONNECTOR_6_0",
+ "BBS2_2": "L22_DISCONNECTOR_6_1",
+ "BBS2_3": "L22_DISCONNECTOR_6_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L6",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L61_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L61_DISCONNECTOR_8_0",
+ "BBS2_2": "L61_DISCONNECTOR_8_1",
+ "BBS2_3": "L61_DISCONNECTOR_8_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L7",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L71_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L71_DISCONNECTOR_10_0",
+ "BBS2_2": "L71_DISCONNECTOR_10_1",
+ "BBS2_3": "L71_DISCONNECTOR_10_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L8",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L81_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "L81_DISCONNECTOR_12_0",
+ "BBS2_2": "L81_DISCONNECTOR_12_1",
+ "BBS2_3": "L81_DISCONNECTOR_12_2"
+ }
+ }
+ },
+ {
+ "grid_model_id": "load1",
+ "type": "LOAD",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "load1_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS2_1": "load1_DISCONNECTOR_18_0",
+ "BBS2_2": "load1_DISCONNECTOR_18_1",
+ "BBS2_3": "load1_DISCONNECTOR_18_2"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ false,
+ true,
+ false,
+ true,
+ true
+ ],
+ [
+ false,
+ true,
+ false,
+ true,
+ false,
+ false
+ ],
+ [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ },
+ {
+ "grid_model_id": "VL3_0",
+ "name": "VLevel3",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS3_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL3_0"
+ },
+ {
+ "grid_model_id": "BBS3_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL3_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL3_BREAKER",
+ "type": "BREAKER",
+ "name": "VL3_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L3",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L32_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L32_DISCONNECTOR_3_0",
+ "BBS3_2": "L32_DISCONNECTOR_3_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L6",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L62_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L62_DISCONNECTOR_5_0",
+ "BBS3_2": "L62_DISCONNECTOR_5_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L7",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L72_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L72_DISCONNECTOR_7_0",
+ "BBS3_2": "L72_DISCONNECTOR_7_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L9",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L91_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "L91_DISCONNECTOR_9_0",
+ "BBS3_2": "L91_DISCONNECTOR_9_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "load2",
+ "type": "LOAD",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "load2_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS3_1": "load2_DISCONNECTOR_13_0",
+ "BBS3_2": "load2_DISCONNECTOR_13_1"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ true,
+ false,
+ true,
+ false
+ ],
+ [
+ false,
+ false,
+ true,
+ false,
+ true
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ },
+ {
+ "grid_model_id": "VL4_0",
+ "name": "VLevel4",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS4_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL4_0"
+ },
+ {
+ "grid_model_id": "BBS4_2",
+ "type": "busbar",
+ "name": "bus2",
+ "int_id": 1,
+ "in_service": true,
+ "bus_branch_bus_id": "VL4_0"
+ }
+ ],
+ "couplers": [
+ {
+ "grid_model_id": "VL4_BREAKER",
+ "type": "BREAKER",
+ "name": "VL4_BREAKER",
+ "busbar_from_id": 0,
+ "busbar_to_id": 1,
+ "open": false,
+ "in_service": true,
+ "asset_bay": null
+ }
+ ],
+ "assets": [
+ {
+ "grid_model_id": "L4",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L42_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS4_1": "L42_DISCONNECTOR_3_0",
+ "BBS4_2": "L42_DISCONNECTOR_3_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L5",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L52_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS4_1": "L52_DISCONNECTOR_5_0",
+ "BBS4_2": "L52_DISCONNECTOR_5_1"
+ }
+ }
+ },
+ {
+ "grid_model_id": "L8",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L82_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS4_1": "L82_DISCONNECTOR_7_0",
+ "BBS4_2": "L82_DISCONNECTOR_7_1"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ false,
+ true
+ ],
+ [
+ false,
+ true,
+ false
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ },
+ {
+ "grid_model_id": "VL5_0",
+ "name": "VLevel5",
+ "type": null,
+ "region": "BE",
+ "voltage_level": 225.0,
+ "busbars": [
+ {
+ "grid_model_id": "BBS5_1",
+ "type": "busbar",
+ "name": "bus1",
+ "int_id": 0,
+ "in_service": true,
+ "bus_branch_bus_id": "VL5_0"
+ }
+ ],
+ "couplers": [],
+ "assets": [
+ {
+ "grid_model_id": "L9",
+ "type": "LINE",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "L92_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS5_1": "L92_DISCONNECTOR_2_0"
+ }
+ }
+ },
+ {
+ "grid_model_id": "generator3",
+ "type": "GENERATOR",
+ "name": "",
+ "in_service": true,
+ "branch_end": null,
+ "asset_bay": {
+ "sl_switch_grid_model_id": null,
+ "dv_switch_grid_model_id": "generator3_BREAKER",
+ "sr_switch_grid_model_id": {
+ "BBS5_1": "generator3_DISCONNECTOR_4_0"
+ }
+ }
+ }
+ ],
+ "asset_switching_table": [
+ [
+ true,
+ true
+ ]
+ ],
+ "asset_connectivity": [
+ [
+ true,
+ true
+ ]
+ ],
+ "model_log": []
+ }
+ ],
+ "asset_setpoints": null,
+ "timestamp": "2026-07-02T10:35:24.879394",
+ "metrics": null
+}
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/original_gridfile/grid.xiidm b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/original_gridfile/grid.xiidm
new file mode 100644
index 000000000..0b6d2fadb
--- /dev/null
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/original_gridfile/grid.xiidm
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/test_ucte_powsybl_example.xiidm b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/test_ucte_powsybl_example.xiidm
deleted file mode 100644
index 6e7c7ea43..000000000
--- a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/test_ucte_powsybl_example.xiidm
+++ /dev/null
@@ -1,359 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json
new file mode 100644
index 000000000..4da43f330
--- /dev/null
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json
@@ -0,0 +1 @@
+{"version": "1.10", "voltageInitMode": "PREVIOUS_VALUES", "transformerVoltageControlOn": true, "phaseShifterRegulationOn": false, "useReactiveLimits": true, "twtSplitShuntAdmittance": true, "shuntCompensatorVoltageControlOn": true, "readSlackBus": true, "writeSlackBus": true, "dc": false, "distributedSlack": true, "balanceType": "PROPORTIONAL_TO_GENERATION_P_MAX", "dcUseTransformerRatio": true, "countriesToBalance": [], "componentMode": "MAIN_CONNECTED", "hvdcAcEmulation": true, "dcPowerFactor": 1.0, "extensions": {"DynaFlowParameters": {"svcRegulationOn": true, "dsoVoltageLevel": 45.0, "tfoVoltageLevel": 100.0, "startTime": 0.0, "stopTime": 100.0, "chosenOutputs": ["TIMELINE"], "timeStep": 10.0, "mergeLoads": true}, "open-load-flow-parameters": {"slackBusSelectionMode": "MOST_MESHED", "slackBusesIds": [], "slackDistributionFailureBehavior": "FAIL", "voltageRemoteControl": true, "voltageRemoteControlRobustMode": true, "lowImpedanceBranchMode": "REPLACE_BY_ZERO_IMPEDANCE_LINE", "loadPowerFactorConstant": false, "plausibleActivePowerLimit": 300.0, "newtonRaphsonStoppingCriteriaType": "PER_EQUATION_TYPE_CRITERIA", "maxActivePowerMismatch": 0.0001, "maxReactivePowerMismatch": 1e-05, "maxVoltageMismatch": 0.0001, "maxAngleMismatch": 1e-05, "maxRatioMismatch": 1e-05, "maxSusceptanceMismatch": 0.0001, "slackBusPMaxMismatch": 1.0, "voltagePerReactivePowerControl": false, "generatorReactivePowerRemoteControl": true, "transformerReactivePowerControl": false, "maxNewtonRaphsonIterations": 25, "maxOuterLoopIterations": 20, "newtonRaphsonConvEpsPerEq": 0.0001, "voltageInitModeOverride": "VOLTAGE_MAGNITUDE", "transformerVoltageControlMode": "INCREMENTAL_VOLTAGE_CONTROL", "shuntVoltageControlMode": "WITH_GENERATOR_VOLTAGE_CONTROL", "minPlausibleTargetVoltage": 0.893, "maxPlausibleTargetVoltage": 1.105, "minNominalVoltageTargetVoltageCheck": 20.0, "minRealisticVoltage": 0.5, "maxRealisticVoltage": 2.0, "minNominalVoltageRealisticVoltageCheck": 0.0, "lowImpedanceThreshold": 1e-08, "reactiveRangeCheckMode": "MAX", "networkCacheEnabled": false, "svcVoltageMonitoring": false, "stateVectorScalingMode": "NONE", "maxSlackBusCount": 1, "debugDir": null, "incrementalTransformerRatioTapControlOuterLoopMaxTapShift": 3, "secondaryVoltageControl": false, "reactiveLimitsMaxPqPvSwitch": 3, "phaseShifterControlMode": "CONTINUOUS_WITH_DISCRETISATION", "alwaysUpdateNetwork": false, "mostMeshedSlackBusSelectorMaxNominalVoltagePercentile": 95.0, "reportedFeatures": [], "slackBusCountryFilter": [], "actionableSwitchesIds": [], "actionableTransformersIds": [], "asymmetrical": false, "reactivePowerDispatchMode": "Q_EQUAL_PROPORTION", "outerLoopNames": null, "useActiveLimits": false, "disableVoltageControlOfGeneratorsOutsideActivePowerLimits": false, "lineSearchStateVectorScalingMaxIteration": 10, "lineSearchStateVectorScalingStepFold": 1.3333333333333333, "maxVoltageChangeStateVectorScalingMaxDv": 0.1, "maxVoltageChangeStateVectorScalingMaxDphi": 0.17453292519943295, "linePerUnitMode": "IMPEDANCE", "useLoadModel": false, "dcApproximationType": "IGNORE_R", "simulateAutomationSystems": false, "acSolverType": "NEWTON_RAPHSON", "maxNewtonKrylovIterations": 100, "newtonKrylovLineSearch": false, "referenceBusSelectionMode": "GENERATOR_REFERENCE_PRIORITY", "writeReferenceTerminals": true, "voltageTargetPriorities": ["VOLTAGE_SOURCE_CONVERTER", "GENERATOR", "TRANSFORMER", "SHUNT"], "transformerVoltageControlUseInitialTapPosition": false, "generatorVoltageControlMinNominalVoltage": -1.0, "fictitiousGeneratorVoltageControlCheckMode": "FORCED", "areaInterchangeControl": false, "areaInterchangeControlAreaType": "ControlArea", "areaInterchangePMaxMismatch": 2.0, "forceTargetQInReactiveLimits": false, "disableInconsistentVoltageControls": false, "extrapolateReactiveLimits": false, "startWithFrozenACEmulation": false, "generatorsWithZeroMwTargetAreNotStarted": true, "incrementalShuntControlOuterLoopMaxSectionShift": 3, "fixVoltageTargets": false, "acDcNetwork": false}}, "provider_parameters": {"voltageInitModeOverride": "VOLTAGE_MAGNITUDE", "reactiveLimitsMaxPpvSwitch": "0", "svcVoltageMonitoring": "false", "maxActivePowerMismatch": "0.0001", "maxReactivePowerMismatch": "0.00001", "maxNewtonRaphsonIterations": "25", "newtonRaphsonStoppingCriteriaType": "PER_EQUATION_TYPE_CRITERIA", "generatorReactivePowerRemoteControl": "true", "plausibleActivePowerLimit": "300", "useActiveLimits": "false", "minPlausibleTargetVoltage": "0.893", "maxPlausibleTargetVoltage": "1.105", "referenceBusSelectionMode": "GENERATOR_REFERENCE_PRIORITY"}}
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/busbar_for_nminus1.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/busbar_for_nminus1.npy
new file mode 100644
index 000000000..47949b0e2
Binary files /dev/null and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/busbar_for_nminus1.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/dangling_line_for_nminus1.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/dangling_line_for_nminus1.npy
new file mode 100644
index 000000000..29df92a58
Binary files /dev/null and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/dangling_line_for_nminus1.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_blacklisted.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_blacklisted.npy
index 4800741fe..9dcf3d787 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_blacklisted.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_blacklisted.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_disconnectable.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_disconnectable.npy
index 104621008..19d80a1c4 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_disconnectable.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_disconnectable.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_nminus1.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_nminus1.npy
index 104621008..19d80a1c4 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_nminus1.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_nminus1.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_reward.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_reward.npy
index 9c42e17cf..c4f97422b 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_reward.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_for_reward.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_overload_weight.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_overload_weight.npy
index 817393592..b00550a51 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_overload_weight.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_overload_weight.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_tso_border.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_tso_border.npy
index 4800741fe..9dcf3d787 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_tso_border.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/line_tso_border.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/load_for_nminus1.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/load_for_nminus1.npy
index 88abb5d6d..2c3097de4 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/load_for_nminus1.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/load_for_nminus1.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/pst_group_labels.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/pst_group_labels.npy
new file mode 100644
index 000000000..d62c53132
Binary files /dev/null and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/pst_group_labels.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/relevant_subs.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/relevant_subs.npy
index b61178756..b67688e7c 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/relevant_subs.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/relevant_subs.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_nminus1.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_nminus1.npy
index 6cc46a518..0a993a881 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_nminus1.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_nminus1.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_reward.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_reward.npy
index 97ab097a1..271af5963 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_reward.npy and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/switch_for_reward.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/trafo_has_pst_tap.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/trafo_has_pst_tap.npy
new file mode 100644
index 000000000..29df92a58
Binary files /dev/null and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/trafo_has_pst_tap.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/trafo_pst_linear.npy b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/trafo_pst_linear.npy
new file mode 100644
index 000000000..29df92a58
Binary files /dev/null and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/trafo_pst_linear.npy differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/network_data.pkl b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/network_data.pkl
deleted file mode 100644
index ffc265408..000000000
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/network_data.pkl and /dev/null differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/nminus1_definition.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/nminus1_definition.json
index 7d3907fa2..9b21eda76 100644
--- a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/nminus1_definition.json
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/nminus1_definition.json
@@ -1,156 +1,201 @@
-{
- "monitored_elements": [
- {
- "id": "L1",
- "type": "LINE",
- "kind": "branch"
- },
- {
- "id": "L2",
- "type": "LINE",
- "kind": "branch"
- },
- {
- "id": "L3",
- "type": "LINE",
- "kind": "branch"
- },
- {
- "id": "VL1_0",
- "type": "BUS",
- "kind": "node"
- },
- {
- "id": "VL2_0",
- "type": "BUS",
- "kind": "node"
- },
- {
- "id": "VL3_0",
- "type": "BUS",
- "kind": "node"
- }
- ],
- "contingencies": [
- {
- "elements": [
- {
- "id": "L1",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L1"
- },
- {
- "elements": [
- {
- "id": "L2",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L2"
- },
- {
- "elements": [
- {
- "id": "L3",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L3"
- },
- {
- "elements": [
- {
- "id": "L4",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L4"
- },
- {
- "elements": [
- {
- "id": "L5",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L5"
- },
- {
- "elements": [
- {
- "id": "L6",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L6"
- },
- {
- "elements": [
- {
- "id": "L7",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L7"
- },
- {
- "elements": [
- {
- "id": "L8",
- "type": "LINE",
- "kind": "branch"
- }
- ],
- "name": "L8"
- },
- {
- "elements": [
- {
- "id": "generator3",
- "type": "GENERATOR",
- "kind": "injection"
- }
- ],
- "name": "generator3"
- },
- {
- "elements": [
- {
- "id": "load2",
- "type": "LOAD",
- "kind": "injection"
- }
- ],
- "name": "load2"
- },
- {
- "elements": [
- {
- "id": "load6",
- "type": "LOAD",
- "kind": "injection"
- }
- ],
- "name": "load6"
- },
- {
- "elements": [
- {
- "id": "load1",
- "type": "LOAD",
- "kind": "injection"
- }
- ],
- "name": "load1"
- }
- ]
-}
+{
+ "monitored_elements": [
+ {
+ "id": "L1",
+ "name": "VL1_3 ## VL2_3 ## ",
+ "type": "LINE",
+ "kind": "branch",
+ "monitoring_scope": null
+ },
+ {
+ "id": "L2",
+ "name": "VL1_5 ## VL2_5 ## ",
+ "type": "LINE",
+ "kind": "branch",
+ "monitoring_scope": null
+ },
+ {
+ "id": "L3",
+ "name": "VL1_7 ## VL3_2 ## ",
+ "type": "LINE",
+ "kind": "branch",
+ "monitoring_scope": null
+ },
+ {
+ "id": "BBS2_1",
+ "name": "bus1",
+ "type": "busbar",
+ "kind": "bus",
+ "monitoring_scope": null
+ },
+ {
+ "id": "BBS2_2",
+ "name": "bus2",
+ "type": "busbar",
+ "kind": "bus",
+ "monitoring_scope": null
+ },
+ {
+ "id": "BBS2_3",
+ "name": "bus3",
+ "type": "busbar",
+ "kind": "bus",
+ "monitoring_scope": null
+ },
+ {
+ "id": "VL2_BREAKER",
+ "name": "VL2_BREAKER",
+ "type": "BREAKER",
+ "kind": "switch",
+ "monitoring_scope": null
+ },
+ {
+ "id": "VL2_BREAKER#0",
+ "name": "VL2_BREAKER#0",
+ "type": "BREAKER",
+ "kind": "switch",
+ "monitoring_scope": null
+ }
+ ],
+ "contingencies": [
+ {
+ "elements": [],
+ "id": "BASECASE",
+ "name": ""
+ },
+ {
+ "elements": [
+ {
+ "id": "L1",
+ "name": "VL1_3 ## VL2_3 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L1",
+ "name": "VL1_3 ## VL2_3 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L2",
+ "name": "VL1_5 ## VL2_5 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L2",
+ "name": "VL1_5 ## VL2_5 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L3",
+ "name": "VL1_7 ## VL3_2 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L3",
+ "name": "VL1_7 ## VL3_2 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L4",
+ "name": "VL1_9 ## VL4_2 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L4",
+ "name": "VL1_9 ## VL4_2 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L5",
+ "name": "VL1_11 ## VL4_4 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L5",
+ "name": "VL1_11 ## VL4_4 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L6",
+ "name": "VL2_7 ## VL3_4 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L6",
+ "name": "VL2_7 ## VL3_4 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L7",
+ "name": "VL2_9 ## VL3_6 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L7",
+ "name": "VL2_9 ## VL3_6 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "L8",
+ "name": "VL2_11 ## VL4_6 ## ",
+ "type": "LINE",
+ "kind": "branch"
+ }
+ ],
+ "id": "L8",
+ "name": "VL2_11 ## VL4_6 ## "
+ },
+ {
+ "elements": [
+ {
+ "id": "generator3",
+ "name": "",
+ "type": "GENERATOR",
+ "kind": "injection"
+ }
+ ],
+ "id": "generator3",
+ "name": ""
+ },
+ {
+ "elements": [
+ {
+ "id": "load2",
+ "name": "",
+ "type": "LOAD",
+ "kind": "injection"
+ }
+ ],
+ "id": "load2",
+ "name": ""
+ },
+ {
+ "elements": [
+ {
+ "id": "load1",
+ "name": "",
+ "type": "LOAD",
+ "kind": "injection"
+ }
+ ],
+ "id": "load1",
+ "name": ""
+ }
+ ],
+ "spps_rules": null,
+ "id_type": null
+}
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.hdf5 b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.hdf5
index df6926229..b8e8b3f75 100644
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.hdf5 and b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.hdf5 differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.zip b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.zip
deleted file mode 100644
index 00359203c..000000000
Binary files a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.zip and /dev/null differ
diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information_stats.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information_stats.json
new file mode 100644
index 000000000..765cc2f12
--- /dev/null
+++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information_stats.json
@@ -0,0 +1,27 @@
+{
+ "time": "2025-10-16 10:10:35.818000+00:00",
+ "fp_dtype": "float64",
+ "has_double_limits": true,
+ "n_branches": 9,
+ "n_nodes": 7,
+ "n_branch_outages": 8,
+ "n_multi_outages": 0,
+ "n_injection_outages": 3,
+ "n_busbar_outages": 0,
+ "n_nminus1_cases": 11,
+ "n_controllable_psts": 0,
+ "n_monitored_branches": 3,
+ "n_timesteps": 1,
+ "n_relevant_subs": 1,
+ "n_disc_branches": 8,
+ "overload_energy_n0": 0.0,
+ "overload_energy_n1": 61.44458010685737,
+ "n_actions": 10,
+ "max_station_branch_degree": 5,
+ "max_station_injection_degree": 1,
+ "mean_station_branch_degree": 5.0,
+ "mean_station_injection_degree": 1.0,
+ "reassignable_branch_assets": 5,
+ "reassignable_injection_assets": 1,
+ "max_reassignment_distance": 4
+}
diff --git a/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py b/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py
index 85a5c7f40..5d50dc57e 100644
--- a/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py
+++ b/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py
@@ -6,14 +6,15 @@
# Mozilla Public License, version 2.0
import copy
-from dataclasses import replace
from pathlib import Path
+from tempfile import TemporaryDirectory
import jax
import jax.numpy as jnp
import numpy as np
import pypowsybl as pp
import pytest
+from fsspec.implementations.dirfs import DirFileSystem
from jaxtyping import Array, Bool, Float, Int
from tests.deprecated.assignment import realise_bus_split_single_station
from toop_engine_dc_solver.jax.bsdf import compute_bus_splits
@@ -45,8 +46,12 @@
int_max,
)
from toop_engine_dc_solver.postprocess.postprocess_powsybl import apply_topology
-from toop_engine_dc_solver.preprocess.convert_to_jax import convert_to_jax, get_bb_outage_baseline_analysis
-from toop_engine_dc_solver.preprocess.network_data import extract_action_set, map_branch_injection_ids
+from toop_engine_dc_solver.preprocess.convert_to_jax import convert_to_jax, get_bb_outage_baseline_analysis, load_grid
+from toop_engine_dc_solver.preprocess.network_data import (
+ extract_action_set,
+ extract_busbar_outage_ids,
+ map_branch_injection_ids,
+)
from toop_engine_dc_solver.preprocess.preprocess import NetworkData
from toop_engine_dc_solver.preprocess.preprocess_bb_outage import (
extract_busbar_outage_data,
@@ -59,7 +64,17 @@
logger,
preprocess_bb_outages,
)
+from toop_engine_grid_helpers.powsybl.example_grids import basic_node_breaker_network_powsybl
+from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK, SINGLE_SLACK
+from toop_engine_importer.pypowsybl_import import preprocessing
from toop_engine_interfaces.asset_topology import Station
+from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS
+from toop_engine_interfaces.messages.preprocess.preprocess_commands import (
+ AreaSettings,
+ CgmesImporterParameters,
+ PreprocessParameters,
+)
+from toop_engine_interfaces.nminus1_definition import load_nminus1_definition
# FIxme: Look deeply into this
@@ -265,6 +280,56 @@ def test_perform_non_rel_bb_outages(
), "Shape of lfs_outage is incorrect"
+def test_node_breaker_import_preserves_all_imported_busbar_outages() -> None:
+ with TemporaryDirectory() as temp_dir_str:
+ temp_dir = Path(temp_dir_str)
+ net = basic_node_breaker_network_powsybl()
+
+ grid_file = temp_dir / "node_breaker_network.xiidm"
+ net.save(grid_file)
+ importer_parameters = CgmesImporterParameters(
+ grid_model_file=grid_file,
+ data_folder=temp_dir,
+ white_list_file=None,
+ black_list_file=None,
+ area_settings=AreaSettings(cutoff_voltage=1, control_area=[""], view_area=[""], nminus1_area=[""]),
+ )
+ preprocessing.convert_file(importer_parameters=importer_parameters)
+
+ imported_nminus1_definition = load_nminus1_definition(temp_dir / PREPROCESSING_PATHS["nminus1_definition_file_path"])
+ all_busbar_ids = sorted(net.get_busbar_sections().index.to_list())
+ expected_busbar_ids = sorted(
+ {
+ element.id
+ for contingency in imported_nminus1_definition.contingencies
+ for element in contingency.elements
+ if element.kind == "bus" and element.type == "BUSBAR_SECTION"
+ }
+ )
+
+ _info, _static_information, network_data = load_grid(
+ data_folder_dirfs=DirFileSystem(str(temp_dir)),
+ pandapower=False,
+ parameters=PreprocessParameters(preprocess_bb_outages=True),
+ lf_params=CGMES_DISTRIBUTED_SLACK,
+ )
+
+ configured_busbar_ids = sorted(
+ busbar_id for busbar_ids in (network_data.busbar_outage_map or {}).values() for busbar_id in busbar_ids
+ )
+ preprocessed_busbar_ids = sorted(extract_busbar_outage_ids(network_data))
+ _, non_rel_busbar_outage_map = get_rel_non_rel_sub_bb_maps(network_data, network_data.busbar_outage_map or {})
+ expected_non_rel_busbar_count = sum(len(busbar_ids) for busbar_ids in non_rel_busbar_outage_map.values())
+
+ assert expected_busbar_ids
+ assert set(expected_busbar_ids).issubset(set(all_busbar_ids))
+ assert configured_busbar_ids == expected_busbar_ids
+ assert preprocessed_busbar_ids == expected_busbar_ids
+ assert len(network_data.non_rel_bb_outage_br_indices) == expected_non_rel_busbar_count
+ assert network_data.non_rel_bb_outage_deltap.shape[0] == expected_non_rel_busbar_count
+ assert network_data.non_rel_bb_outage_nodal_indices.shape[0] == expected_non_rel_busbar_count
+
+
def test_perform_rel_bb_outage_single_topo_with_no_inj_reassignments(
jax_inputs_oberrhein: tuple[ActionIndexComputations, StaticInformation],
network_data_preprocessed: NetworkData,
@@ -473,15 +538,14 @@ def correct_power_flow_directions(lfs, network_data_preprocessed: NetworkData):
def test_compare_loadflows_non_rel_bb_outage_powsybl(
test_grid_folder_path: Path,
network_data_test_grid: NetworkData,
- outage_map_test_grid: dict[str, list[str]],
):
net = pp.network.load(test_grid_folder_path / "grid.xiidm")
# outage_map = outage_map_test_grid
outage_map = network_data_test_grid.busbar_outage_map
+ assert set(outage_map.keys()) == {"VL2_0", "VL3_0", "VL4_0", "VL5_0"}
rel_bb_outage_map, non_rel_bb_outage_map = get_rel_non_rel_sub_bb_maps(network_data_test_grid, outage_map)
- network_data = replace(network_data_test_grid, busbar_outage_map=outage_map)
- network_data = preprocess_bb_outages(network_data)
+ network_data = preprocess_bb_outages(network_data_test_grid)
static_information = convert_to_jax(
network_data,
preprocess_bb_outages=True,
@@ -513,10 +577,7 @@ def test_compare_loadflows_non_rel_bb_outage_powsybl(
network_data.branch_ids.index(asset.grid_model_id) for asset in connected_assets if asset.is_branch()
]
copy_net.remove_elements(connected_asset_ids)
- config = pp.loadflow.Parameters(
- distributed_slack=False,
- )
- pp.loadflow.run_dc(copy_net, parameters=config)
+ pp.loadflow.run_dc(copy_net, parameters=SINGLE_SLACK)
# Get the stub branches connected to this busbar
stub_branch_indices = []
@@ -544,7 +605,6 @@ def test_compare_loadflows_non_rel_bb_outage_powsybl(
def test_compare_loadflows_rel_bb_outage(
test_grid_folder_path: Path,
network_data_test_grid: NetworkData,
- outage_map_test_grid: dict[str, list[str]],
jax_inputs_test_grid: tuple[ActionIndexComputations, StaticInformation],
):
net = pp.network.load(test_grid_folder_path / "grid.xiidm")
@@ -556,7 +616,10 @@ def test_compare_loadflows_rel_bb_outage(
# These indices are neccessarily sorted in the order of rel_subs
updated_topo_indices = jax.jit(pad_action_with_unsplit_action_indices)(di.action_set, topo_indices.action)
- rel_bb_outage_map, _ = get_rel_non_rel_sub_bb_maps(network_data_test_grid, outage_map_test_grid)
+
+ assert set(network_data_test_grid.busbar_outage_map.keys()) == {"VL2_0", "VL3_0", "VL4_0", "VL5_0"}
+
+ rel_bb_outage_map, _ = get_rel_non_rel_sub_bb_maps(network_data_test_grid, network_data_test_grid.busbar_outage_map)
branch_actions = di.action_set.branch_actions[updated_topo_indices]
affected_sub_ids = di.action_set.substation_correspondence[updated_topo_indices]
@@ -605,10 +668,7 @@ def test_compare_loadflows_rel_bb_outage(
if asset.is_branch()
]
copy_net.remove_elements(connected_asset_ids)
- config = pp.loadflow.Parameters(
- distributed_slack=False,
- )
- pp.loadflow.run_dc(copy_net, parameters=config)
+ pp.loadflow.run_dc(copy_net, parameters=SINGLE_SLACK)
# Get the stub branches connected to this busbar
stub_branch_indices = []
diff --git a/packages/dc_solver_pkg/tests/jax/test_compute_batch.py b/packages/dc_solver_pkg/tests/jax/test_compute_batch.py
index 43098d3ba..6f317c642 100644
--- a/packages/dc_solver_pkg/tests/jax/test_compute_batch.py
+++ b/packages/dc_solver_pkg/tests/jax/test_compute_batch.py
@@ -227,6 +227,50 @@ def test_compute_batch_symmetric_with_bb_outage(
assert lf_res.bb_outage_penalty.shape == (solver_config.batch_size_bsdf,)
+def test_compute_batch_symmetric_with_bb_outage_nminus1_with_baseline(
+ jax_inputs_oberrhein: tuple[ActionIndexComputations, StaticInformation],
+) -> None:
+ """Regression test: keep baseline present with bb_outage_as_nminus1 enabled.
+
+ In this setup, N-1 contains busbar outages and contingency_success must have
+ matching failure-axis length.
+ """
+ topo_indices, static_information = jax_inputs_oberrhein
+ di = static_information.dynamic_information
+ baseline_analysis = di.bb_outage_baseline_analysis
+ if baseline_analysis is None:
+ baseline_analysis = get_bb_outage_baseline_analysis(
+ di=di,
+ more_splits_penalty=1000.0,
+ )
+
+ static_information = replace(
+ static_information,
+ solver_config=replace(
+ static_information.solver_config,
+ enable_bb_outages=True,
+ bb_outage_as_nminus1=True,
+ ),
+ dynamic_information=replace(
+ di,
+ bb_outage_baseline_analysis=baseline_analysis,
+ ),
+ )
+
+ lf_res, success = compute_symmetric_batch(
+ topology_batch=topo_indices,
+ disconnection_batch=None,
+ injections=None,
+ nodal_inj_start_options=None,
+ dynamic_information=static_information.dynamic_information,
+ solver_config=static_information.solver_config,
+ )
+
+ assert jnp.any(success)
+ assert lf_res.contingency_success is not None
+ assert lf_res.contingency_success.shape[1] == lf_res.n_1_matrix.shape[2]
+
+
def test_compute_symmetric_batch_with_disconnection(
jax_inputs: tuple[TopoVectBranchComputations, InjectionComputations, StaticInformation],
) -> None:
diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py b/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py
index 0b1576f55..35e921dbf 100644
--- a/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py
+++ b/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py
@@ -247,6 +247,7 @@ def test_apply_disconnections_matches_loadflows(
"complex_grid_battery_hvdc_svc_3w_trafo_linear_1_0_data_folder",
"complex_grid_battery_hvdc_svc_3w_trafo_linear_1_1_data_folder",
"complex_grid_battery_hvdc_svc_3w_trafo_linear_0_1_data_folder",
+ "grouped_pst_grid_example_data_folder",
],
)
def test_change_pst_matches_loadflows(
diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_convert_to_jax.py b/packages/dc_solver_pkg/tests/preprocessing/test_convert_to_jax.py
index a7ec1f0a4..2982d0f74 100644
--- a/packages/dc_solver_pkg/tests/preprocessing/test_convert_to_jax.py
+++ b/packages/dc_solver_pkg/tests/preprocessing/test_convert_to_jax.py
@@ -232,6 +232,29 @@ def test_convert_rel_bb_outage_data(network_data_preprocessed: NetworkData, ober
assert len(network_data_preprocessed.non_rel_bb_outage_nodal_indices) == 0
+def test_convert_rel_bb_outage_data_uses_physical_busbar_width_for_articulation_mask(
+ network_data_preprocessed: NetworkData,
+) -> None:
+ n_timesteps = network_data_preprocessed.nodal_injection.shape[0]
+ rel_bb_outage_data = convert_rel_bb_outage_data(
+ replace(
+ network_data_preprocessed,
+ branch_action_set=[np.zeros((1, 1), dtype=bool)],
+ rel_bb_outage_br_indices=[[[[], [], [], []]]],
+ rel_bb_outage_deltap=[
+ [[np.zeros(n_timesteps), np.zeros(n_timesteps), np.zeros(n_timesteps), np.zeros(n_timesteps)]]
+ ],
+ rel_bb_outage_nodal_indices=[[[0, 1, 2, 3]]],
+ rel_bb_articulation_nodes=[[[4]]],
+ )
+ )
+
+ assert rel_bb_outage_data.branch_outage_set.shape[1] == 5
+ assert rel_bb_outage_data.nodal_indices.shape[1] == 5
+ assert rel_bb_outage_data.articulation_node_mask.shape[1] == 5
+ assert rel_bb_outage_data.articulation_node_mask[0, 4]
+
+
def test_get_bb_outage_baseline_analysis(jax_inputs_oberrhein):
static_information = jax_inputs_oberrhein[1]
result = get_bb_outage_baseline_analysis(
diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_backend.py b/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_backend.py
index 4bd155b73..bdff4fb2f 100644
--- a/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_backend.py
+++ b/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_backend.py
@@ -37,6 +37,7 @@
from toop_engine_dc_solver.preprocess.preprocess import preprocess
from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK
from toop_engine_interfaces.folder_structure import (
+ NETWORK_MASK_NAMES,
PREPROCESSING_PATHS,
)
from toop_engine_interfaces.loadflow_result_helpers_polars import extract_solver_matrices_polars
@@ -96,6 +97,84 @@ def test_get_nodes(powsybl_case57_folder_xiidm: Path) -> None:
assert backend.get_cross_coupler_limits().shape == (n_connected_nodes,)
+def test_get_busbar_outage_map(powsybl_data_folder: Path) -> None:
+ filesystem_dir_powsybl = DirFileSystem(str(powsybl_data_folder))
+ backend = PowsyblBackend(filesystem_dir_powsybl)
+ asset_topology = backend.get_asset_topology()
+
+ busbar_sections = backend.net.get_busbar_sections(attributes=["bus_id"])
+ connected_busbars = busbar_sections[busbar_sections["bus_id"].isin(backend.get_node_ids())]
+ selected_busbars = connected_busbars.iloc[: min(3, len(connected_busbars))]
+ mask = np.zeros(len(busbar_sections), dtype=bool)
+ mask[busbar_sections.index.get_indexer(selected_busbars.index)] = True
+ mask_path = powsybl_data_folder / PREPROCESSING_PATHS["masks_path"] / NETWORK_MASK_NAMES["busbar_for_nminus1"]
+ mask_path.parent.mkdir(parents=True, exist_ok=True)
+ np.save(mask_path, mask)
+
+ selected_busbars = busbar_sections[mask]
+ selected_busbars = selected_busbars[selected_busbars["bus_id"].isin(backend.get_node_ids())]
+ busbar_to_station_id = {}
+ bus_id_to_station_id = {}
+ if asset_topology is not None:
+ busbar_to_station_id = {
+ busbar.grid_model_id: station.grid_model_id for station in asset_topology.stations for busbar in station.busbars
+ }
+ bus_id_to_station_id = {
+ busbar.bus_branch_bus_id: station.grid_model_id
+ for station in asset_topology.stations
+ for busbar in station.busbars
+ }
+ expected_outage_map: dict[str, list[str]] = {}
+ for busbar_id, busbar in selected_busbars.iterrows():
+ station_id = busbar_to_station_id.get(str(busbar_id))
+ if station_id is None:
+ station_id = bus_id_to_station_id.get(str(busbar["bus_id"]))
+ if station_id is None:
+ continue
+ expected_outage_map.setdefault(station_id, []).append(str(busbar_id))
+
+ assert backend.get_busbar_outage_map() == expected_outage_map
+
+
+def test_get_busbar_outage_map_case57(powsybl_case57_folder_xiidm: Path) -> None:
+ filesystem_dir_powsybl = DirFileSystem(str(powsybl_case57_folder_xiidm))
+ backend = PowsyblBackend(filesystem_dir_powsybl)
+ asset_topology = backend.get_asset_topology()
+
+ busbar_sections = backend.net.get_busbar_sections(attributes=["bus_id"])
+ connected_busbars = busbar_sections[busbar_sections["bus_id"].isin(backend.get_node_ids())]
+ selected_busbars = connected_busbars.iloc[: min(3, len(connected_busbars))]
+ mask = np.zeros(len(busbar_sections), dtype=bool)
+ mask[busbar_sections.index.get_indexer(selected_busbars.index)] = True
+ mask_path = powsybl_case57_folder_xiidm / PREPROCESSING_PATHS["masks_path"] / NETWORK_MASK_NAMES["busbar_for_nminus1"]
+ mask_path.parent.mkdir(parents=True, exist_ok=True)
+ np.save(mask_path, mask)
+
+ selected_busbars = busbar_sections[mask]
+ selected_busbars = selected_busbars[selected_busbars["bus_id"].isin(backend.get_node_ids())]
+ busbar_to_station_id = {}
+ bus_id_to_station_id = {}
+ if asset_topology is not None:
+ busbar_to_station_id = {
+ busbar.grid_model_id: station.grid_model_id for station in asset_topology.stations for busbar in station.busbars
+ }
+ bus_id_to_station_id = {
+ busbar.bus_branch_bus_id: station.grid_model_id
+ for station in asset_topology.stations
+ for busbar in station.busbars
+ }
+ expected_outage_map: dict[str, list[str]] = {}
+ for busbar_id, busbar in selected_busbars.iterrows():
+ station_id = busbar_to_station_id.get(str(busbar_id))
+ if station_id is None:
+ station_id = bus_id_to_station_id.get(str(busbar["bus_id"]))
+ if station_id is None:
+ continue
+ expected_outage_map.setdefault(station_id, []).append(str(busbar_id))
+
+ assert backend.get_busbar_outage_map() == expected_outage_map
+
+
def test_get_injections(powsybl_case57_folder_xiidm: Path) -> None:
filesystem_dir_powsybl = DirFileSystem(str(powsybl_case57_folder_xiidm))
backend = PowsyblBackend(filesystem_dir_powsybl)
@@ -429,3 +508,42 @@ def test_psts(tmp_path_factory: pytest.TempPathFactory) -> None:
tap_found = True
break
assert tap_found
+
+
+@pytest.mark.parametrize(
+ (
+ "fixture_name",
+ "expected_group_mask",
+ "expected_group_ids",
+ ),
+ [
+ (
+ "grouped_pst_grid_example_data_folder",
+ np.ones((1, 4), dtype=bool),
+ ["PST_1_group_1"],
+ ),
+ (
+ "grouped_pst_grid_example_split_data_folder",
+ np.array([[True, False, True, False], [False, True, False, True]], dtype=bool),
+ ["PST_1_group_1", "PST_2_group_1"],
+ ),
+ ],
+)
+def test_grouped_pst_grid_backend_reads_parallel_group_masks(
+ request: pytest.FixtureRequest,
+ fixture_name: str,
+ expected_group_mask: np.ndarray,
+ expected_group_ids: list[str],
+) -> None:
+ data_folder = request.getfixturevalue(fixture_name)
+ backend = PowsyblBackend(DirFileSystem(str(data_folder)))
+
+ assert backend.get_controllable_phase_shift_ids() == [
+ "PST_1_group_1",
+ "PST_2_group_1",
+ "PST_3_group_2",
+ "PST_4_group_2",
+ ]
+ assert np.array_equal(backend.get_phase_shift_linearity(), np.ones(4, dtype=bool))
+ assert np.array_equal(backend.get_parallel_pst_group_mask(), expected_group_mask)
+ assert backend.get_parallel_pst_group_ids() == expected_group_ids
diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_helpers.py b/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_helpers.py
index 95e5821fc..cdc14984d 100644
--- a/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_helpers.py
+++ b/packages/dc_solver_pkg/tests/preprocessing/test_powsybl_helpers.py
@@ -15,12 +15,14 @@
import pytest
from toop_engine_dc_solver.preprocess.powsybl.powsybl_helpers import (
add_missing_branch_model_columns,
+ get_linear_pst,
get_lines,
get_network_as_pu,
get_p_max,
get_tie_lines,
get_trafos,
)
+from toop_engine_grid_helpers.powsybl.example_grids import grouped_pst_grid_example
from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS
@@ -290,3 +292,25 @@ def test_get_cgmes_ids_hybrid(ucte_file: Path) -> None:
cgmes_ids = get_cgmes_ids(net)
assert np.array_equal(cgmes_ids, cgmes_net.get_identifiables().index.values)
+
+
+@pytest.mark.parametrize(
+ ("linear_pst", "expected_linearity"),
+ [
+ (None, [True, True, True, True]),
+ ([True, True, True, True], [True, True, True, True]),
+ ([False, False, False, False], [False, False, False, False]),
+ ([True, False, True, False], [True, False, True, False]),
+ ],
+)
+def test_grouped_pst_grid_importer_detects_linear_and_nonlinear_psts(
+ linear_pst: list[bool] | None, expected_linearity: list[bool]
+) -> None:
+ net = grouped_pst_grid_example(linear_pst=linear_pst)
+ pst_ids = ["PST_1_group_1", "PST_2_group_1", "PST_3_group_2", "PST_4_group_2"]
+
+ dc_linearity = get_linear_pst(net, mode="dc").loc[pst_ids]
+ ac_linearity = get_linear_pst(net, mode="ac").loc[pst_ids]
+
+ assert np.array_equal(dc_linearity.values, np.array(expected_linearity))
+ assert np.array_equal(ac_linearity.values, np.array(expected_linearity))
diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_write_aux_data.py b/packages/dc_solver_pkg/tests/preprocessing/test_write_aux_data.py
index a1c1b298f..bb1e82570 100644
--- a/packages/dc_solver_pkg/tests/preprocessing/test_write_aux_data.py
+++ b/packages/dc_solver_pkg/tests/preprocessing/test_write_aux_data.py
@@ -13,7 +13,12 @@
from toop_engine_dc_solver.example_grids import case30_with_psts_pandapower
from toop_engine_dc_solver.postprocess.write_aux_data import write_aux_data
from toop_engine_dc_solver.preprocess.convert_to_jax import convert_to_jax
-from toop_engine_dc_solver.preprocess.network_data import NetworkData, extract_action_set, extract_nminus1_definition
+from toop_engine_dc_solver.preprocess.network_data import (
+ NetworkData,
+ extract_action_set,
+ extract_busbar_outage_ids,
+ extract_nminus1_definition,
+)
from toop_engine_dc_solver.preprocess.pandapower.pandapower_backend import PandaPowerBackend
from toop_engine_dc_solver.preprocess.preprocess import preprocess
from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS
@@ -24,13 +29,20 @@
def test_extract_data_compare_to_jax(network_data_preprocessed: NetworkData) -> None:
nminus1_definition = extract_nminus1_definition(network_data_preprocessed)
action_set = extract_action_set(network_data_preprocessed)
+ busbar_outage_ids = extract_busbar_outage_ids(network_data_preprocessed)
+ busbar_contingencies = [
+ contingency
+ for contingency in nminus1_definition.contingencies
+ if contingency.elements and contingency.elements[0].kind == "bus"
+ ]
# Compare with the processed jax data
static_information = convert_to_jax(network_data_preprocessed)
assert len(action_set.local_actions) == len(static_information.dynamic_information.action_set)
mon_branches = [el for el in nminus1_definition.monitored_elements if el.kind == "branch"]
assert len(mon_branches) == static_information.n_branches_monitored
- assert len(nminus1_definition.contingencies) == static_information.n_nminus1_cases + 1
+ assert [contingency.id for contingency in busbar_contingencies] == busbar_outage_ids
+ assert len(nminus1_definition.contingencies) == static_information.n_nminus1_cases + len(busbar_outage_ids) + 1
assert nminus1_definition.contingencies[0].id == "BASECASE"
assert len(nminus1_definition.contingencies[0].elements) == 0
assert len(action_set.disconnectable_branches) == static_information.dynamic_information.disconnectable_branches.shape[0]
@@ -45,12 +57,14 @@ def test_extract_data_compare_to_network_data(network_data_preprocessed: Network
n_multi_outages = len(network_data_preprocessed.multi_outage_ids)
n_branch_outages = network_data_preprocessed.outaged_branch_mask.sum()
n_inj_outages = network_data_preprocessed.outaged_injection_mask.sum()
+ n_busbar_outages = len(extract_busbar_outage_ids(network_data_preprocessed))
n_expected_contingencies = (
1 # Base case
+ n_branch_outages # Single branch outages
+ n_multi_outages # Multi outages
+ n_inj_outages # Injection outages
+ + n_busbar_outages # Exported busbar outages
)
assert n_contingencies == n_expected_contingencies, "Number of contingencies does not match expected count"
diff --git a/packages/dc_solver_pkg/tests/test_example_grids.py b/packages/dc_solver_pkg/tests/test_example_grids.py
index 133d8cde6..dcabcf690 100644
--- a/packages/dc_solver_pkg/tests/test_example_grids.py
+++ b/packages/dc_solver_pkg/tests/test_example_grids.py
@@ -14,6 +14,7 @@
import pypowsybl
import pytest
from fsspec.implementations.dirfs import DirFileSystem
+from tests.network_data_pickle import load_network_data
from tests.numpy_reference import calc_bsdf as calc_bsdf_numpy
from toop_engine_dc_solver.example_grids import (
PandapowerCounters,
@@ -33,7 +34,7 @@
random_topology_info_backend,
)
from toop_engine_dc_solver.jax.bsdf import _apply_bus_split, calc_bsdf, init_bsdf_results
-from toop_engine_dc_solver.jax.inputs import validate_static_information
+from toop_engine_dc_solver.jax.inputs import load_static_information_fs, validate_static_information
from toop_engine_dc_solver.jax.lodf import calc_lodf
from toop_engine_dc_solver.preprocess.convert_to_jax import convert_to_jax, load_grid
from toop_engine_dc_solver.preprocess.pandapower.pandapower_backend import PandaPowerBackend
@@ -469,6 +470,53 @@ def test_case30_with_psts_powsybl() -> None:
assert powsybl_backend.get_controllable_phase_shift_mask().sum() == 2
+def test_parallel_pst_example_data_folder(parallel_pst_data_folder: Path) -> None:
+ network_data = load_network_data(parallel_pst_data_folder / "network_data.pkl")
+ assert network_data.controllable_phase_shift_mask.sum() == 3
+ assert np.array_equal(network_data.phase_shift_linearity, np.array([True, True, True]))
+ assert np.array_equal(
+ network_data.parallel_pst_group_mask,
+ np.array([[True, True, False], [False, False, True]], dtype=bool),
+ )
+ assert network_data.parallel_pst_group_ids == ["PST1", "PST3"]
+ assert len(network_data.branch_action_set) > 0
+
+
+@pytest.mark.parametrize(
+ ("fixture_name", "expected_group_mask", "expected_group_ids"),
+ [
+ ("grouped_pst_grid_example_data_folder", np.ones((1, 4), dtype=bool), ["PST_1_group_1"]),
+ (
+ "grouped_pst_grid_example_split_data_folder",
+ np.array([[True, False, True, False], [False, True, False, True]], dtype=bool),
+ ["PST_1_group_1", "PST_2_group_1"],
+ ),
+ ],
+)
+def test_grouped_pst_grid_example_data_folder(
+ request: pytest.FixtureRequest,
+ fixture_name: str,
+ expected_group_mask: np.ndarray,
+ expected_group_ids: list[str],
+) -> None:
+ data_folder = request.getfixturevalue(fixture_name)
+ network_data = load_network_data(data_folder / "network_data.pkl")
+
+ assert network_data.controllable_phase_shift_mask.sum() == 4
+ assert np.array_equal(network_data.phase_shift_linearity, np.ones(4, dtype=bool))
+ assert np.array_equal(network_data.parallel_pst_group_mask, expected_group_mask)
+ assert network_data.parallel_pst_group_ids == expected_group_ids
+ assert len(network_data.branch_action_set) > 0
+
+ static_information = load_static_information_fs(
+ filesystem=DirFileSystem(str(data_folder)),
+ filename=PREPROCESSING_PATHS["static_information_file_path"],
+ )
+ nodal_injection_information = static_information.dynamic_information.nodal_injection_information
+ assert nodal_injection_information is not None
+ assert np.array_equal(nodal_injection_information.parallel_pst_group_mask, expected_group_mask)
+
+
def test_case14_with_matching_asset_topo() -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_dir = Path(tmp_dir)
diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
index f52d19755..1d5421bfc 100644
--- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
+++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
@@ -303,7 +303,7 @@ def _prepare_basic_node_breaker_network_powsybl(
busbar_section_position = pd.DataFrame.from_records(
index="id",
data=[
- {"id": f"BBS{sub_id}_{bus_id}", "section_index": 1, "busbar_index": bus_id}
+ {"id": f"BBS{sub_id}_{bus_id}", "section_index": 1, "busbar_index": int(bus_id)}
for sub_id, num_buses in n_buses.items()
for bus_id in range(1, num_buses + 1)
],
@@ -313,6 +313,8 @@ def _prepare_basic_node_breaker_network_powsybl(
for busbar_id, updated_fields in busbar_section_position_updates.items():
for field, value in updated_fields.items():
busbar_section_position.loc[busbar_id, field] = value
+ busbar_section_position["section_index"] = busbar_section_position["section_index"].astype(int)
+ busbar_section_position["busbar_index"] = busbar_section_position["busbar_index"].astype(int)
net.create_substations(stations)
net.create_voltage_levels(voltage_levels)
@@ -1642,6 +1644,8 @@ def _create_busbars(voltage_list: list, kwargs: dict) -> None:
if connect_line_out_of_service:
n.connect("LINE_out_of_service")
+ pypowsybl.loadflow.run_ac(n)
+
return n
@@ -2332,3 +2336,208 @@ def parallel_pst_example() -> Network:
net.create_operational_limits(df=limits)
return net
+
+
+def grouped_pst_grid_example(linear_pst: list[bool] | None) -> pypowsybl.network.Network:
+ """Create an extended node breaker network with parallel PSTs in a two-sided station.
+
+ Base setup: two lines are overloaded.
+
+ Solution 1: PST only, one line overloaded, other reduced
+ net.update_phase_tap_changers(id="PST_1_group_1", tap=-7)
+ net.update_phase_tap_changers(id="PST_2_group_1", tap=-7)
+ net.update_phase_tap_changers(id="PST_3_group_2", tap=-7)
+ net.update_phase_tap_changers(id="PST_4_group_2", tap=-7)
+
+ Solution 2: PST + bus split, no overloads
+ net.open_switch("VL2_BREAKER#0")
+ net.update_phase_tap_changers(id="PST_1_group_1", tap=-7)
+ net.update_phase_tap_changers(id="PST_2_group_1", tap=-7)
+ net.update_phase_tap_changers(id="PST_3_group_2", tap=-6)
+ net.update_phase_tap_changers(id="PST_4_group_2", tap=-6)
+
+ Parameters
+ ----------
+ linear_pst : list[bool] | None
+ A boolean list of length 4 indicating whether each of the four PSTs should be linear (True)
+ or non-linear (False). If None, all PSTs are linear.
+
+ Returns
+ -------
+ pypowsybl.network.Network
+ The created Powsybl network.
+ """
+ if linear_pst is None:
+ linear_pst = [True, True, True, True]
+ if len(linear_pst) != 4:
+ msg = "linear_pst must contain exactly 4 boolean values."
+ raise ValueError(msg)
+ # substation_id : number of buses
+ n_buses = {1: 3, 2: 0, 3: 2, 4: 2, 5: 1}
+ net = _prepare_basic_node_breaker_network_powsybl(n_subs=5, n_vls=5, n_buses=n_buses)
+
+ pypowsybl.network.create_voltage_level_topology(
+ net,
+ id="VL2",
+ aligned_buses_or_busbar_count=2,
+ switch_kinds="BREAKER, DISCONNECTOR, BREAKER",
+ bus_or_busbar_section_prefix_id="BBS2_",
+ )
+ # remove switches to create a two sided station for the PST
+ net.remove_elements(["VL2_DISCONNECTOR_2_4", "VL2_DISCONNECTOR_3_5"])
+ lines = pd.DataFrame.from_records(
+ data=[
+ {"bus_or_busbar_section_id_1": "BBS1_1", "bus_or_busbar_section_id_2": "VL2_1_1"},
+ {"bus_or_busbar_section_id_1": "BBS1_2", "bus_or_busbar_section_id_2": "VL2_1_2"},
+ {"bus_or_busbar_section_id_1": "BBS1_3", "bus_or_busbar_section_id_2": "BBS3_1"},
+ {"bus_or_busbar_section_id_1": "BBS3_1", "bus_or_busbar_section_id_2": "BBS4_1"},
+ {"bus_or_busbar_section_id_1": "VL2_1_3", "bus_or_busbar_section_id_2": "BBS3_1"},
+ {"bus_or_busbar_section_id_1": "VL2_2_3", "bus_or_busbar_section_id_2": "BBS3_2"},
+ {"bus_or_busbar_section_id_1": "VL2_1_4", "bus_or_busbar_section_id_2": "BBS4_1"},
+ {"bus_or_busbar_section_id_1": "VL2_2_4", "bus_or_busbar_section_id_2": "BBS4_2"},
+ {"bus_or_busbar_section_id_1": "BBS3_1", "bus_or_busbar_section_id_2": "BBS5_1"},
+ ]
+ )
+ lines["r"] = 0.1
+ lines["x"] = 10
+ lines["g1"] = 0
+ lines["b1"] = 0
+ lines["g2"] = 0
+ lines["b2"] = 0
+ lines["position_order_1"] = 1
+ lines["position_order_2"] = 1
+ for i, _ in lines.iterrows():
+ lines.loc[i, "id"] = f"L{i + 1}"
+ lines = lines.set_index("id")
+ pypowsybl.network.create_line_bays(net, lines)
+
+ pypowsybl.network.create_coupling_device(
+ net, bus_or_busbar_section_id_1=["BBS1_1", "BBS1_2"], bus_or_busbar_section_id_2=["BBS1_2", "BBS1_3"]
+ )
+ # pst station
+ # vertical busbars
+ pypowsybl.network.create_coupling_device(
+ net, bus_or_busbar_section_id_1=["VL2_1_1"], bus_or_busbar_section_id_2=["VL2_2_1"]
+ )
+ pypowsybl.network.create_coupling_device(
+ net, bus_or_busbar_section_id_1=["VL2_1_3"], bus_or_busbar_section_id_2=["VL2_2_3"]
+ )
+
+ pypowsybl.network.create_coupling_device(
+ net, bus_or_busbar_section_id_1=["BBS3_1"], bus_or_busbar_section_id_2=["BBS3_2"]
+ )
+ pypowsybl.network.create_coupling_device(
+ net, bus_or_busbar_section_id_1=["BBS4_1"], bus_or_busbar_section_id_2=["BBS4_2"]
+ )
+ pypowsybl.network.create_load_bay(net, id="load1", bus_or_busbar_section_id="BBS4_1", p0=100, q0=10, position_order=2)
+ pypowsybl.network.create_load_bay(net, id="load2", bus_or_busbar_section_id="BBS5_1", p0=100, q0=10, position_order=2)
+ pypowsybl.network.create_generator_bay(
+ net,
+ id="generator1",
+ max_p=1000,
+ min_p=0,
+ voltage_regulator_on=True,
+ target_p=200,
+ target_q=30,
+ target_v=225,
+ bus_or_busbar_section_id="BBS1_1",
+ position_order=1,
+ )
+
+ def _create_pst_df(
+ id: str, bus_or_busbar_section_id_1: str, bus_or_busbar_section_id_2: str, linear: bool = False
+ ) -> None:
+ """Helper function to create a phase tap changer with a given id and associated steps."""
+
+ pypowsybl.network.create_2_windings_transformer_bays(
+ net,
+ id=id,
+ b=1e-6,
+ g=5e-7,
+ r=0.1,
+ x=12.0,
+ rated_u1=225.0,
+ rated_u2=225.0,
+ bus_or_busbar_section_id_1=bus_or_busbar_section_id_1,
+ position_order_1=50,
+ direction_1="BOTTOM",
+ bus_or_busbar_section_id_2=bus_or_busbar_section_id_2,
+ position_order_2=50,
+ direction_2="BOTTOM",
+ )
+ ptc_df = pd.DataFrame.from_records(
+ index="id",
+ columns=["id", "target_deadband", "regulation_mode", "low_tap", "tap"],
+ data=[(id, 2, "CURRENT_LIMITER", -30, -20)],
+ )
+ # base/min/max values (keep b,g,r,x constant as before, interpolate rho and alpha)
+ taps = np.arange(-30, 20)
+ b_val, g_val, rho_val = 0, 0, 1
+ alpha_min, alpha_max = -21.0, 28.0
+ x_min, x_max = -20.0, 30.0
+ r_min, r_max = -15.0, 25.0
+
+ alphas = np.linspace(alpha_min, alpha_max, len(taps))
+ x_vals = abs(np.linspace(x_min, x_max, len(taps)))
+ r_vals = abs(np.linspace(r_min, r_max, len(taps)))
+
+ if linear:
+ x_vals = np.zeros_like(x_vals)
+ r_vals = np.zeros_like(r_vals)
+
+ rows = [
+ (id, b_val, g_val, r_val, x_val, rho_val, alpha)
+ for r_val, x_val, alpha in zip(r_vals, x_vals, alphas, strict=True)
+ ]
+
+ steps_df = pd.DataFrame.from_records(data=rows, index="id", columns=["id", "b", "g", "r", "x", "rho", "alpha"])
+
+ net.create_phase_tap_changers(ptc_df, steps_df)
+
+ _create_pst_df(
+ id="PST_1_group_1", bus_or_busbar_section_id_1="VL2_1_1", bus_or_busbar_section_id_2="VL2_1_3", linear=linear_pst[0]
+ )
+ _create_pst_df(
+ id="PST_2_group_1", bus_or_busbar_section_id_1="VL2_2_1", bus_or_busbar_section_id_2="VL2_2_3", linear=linear_pst[1]
+ )
+ _create_pst_df(
+ id="PST_3_group_2", bus_or_busbar_section_id_1="VL2_1_2", bus_or_busbar_section_id_2="VL2_1_4", linear=linear_pst[2]
+ )
+ _create_pst_df(
+ id="PST_4_group_2", bus_or_busbar_section_id_1="VL2_2_2", bus_or_busbar_section_id_2="VL2_2_4", linear=linear_pst[3]
+ )
+
+ limits = pd.DataFrame.from_records(
+ data=[
+ {
+ "element_id": "L3",
+ "value": 50,
+ "side": "ONE",
+ "name": "permanent_limit",
+ "type": "CURRENT",
+ "acceptable_duration": -1,
+ },
+ {
+ "element_id": "L4",
+ "value": 20,
+ "side": "ONE",
+ "name": "permanent_limit",
+ "type": "CURRENT",
+ "acceptable_duration": -1,
+ },
+ ],
+ index="element_id",
+ )
+ net.create_operational_limits(limits)
+ slack_voltage_id = "VL1"
+ slack_bus_id = "VL1_0"
+ dict_slack = {"voltage_level_id": slack_voltage_id, "bus_id": slack_bus_id}
+ pypowsybl.network.Network.create_extensions(net, extension_name="slackTerminal", **dict_slack)
+ # set taps to neutral position
+ net.update_phase_tap_changers(id="PST_1_group_1", tap=-9)
+ net.update_phase_tap_changers(id="PST_2_group_1", tap=-9)
+ net.update_phase_tap_changers(id="PST_3_group_2", tap=-9)
+ net.update_phase_tap_changers(id="PST_4_group_2", tap=-9)
+ pypowsybl.loadflow.run_ac(net)
+
+ return net
diff --git a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_example_grids.py b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_example_grids.py
index ca5d5d90b..78c017887 100644
--- a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_example_grids.py
+++ b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_example_grids.py
@@ -13,6 +13,7 @@
basic_node_breaker_network_powsybl_v2,
create_complex_grid_battery_hvdc_svc_3w_trafo,
create_complex_substation_layout_grid,
+ grouped_pst_grid_example,
parallel_pst_example,
powsybl_case30_with_psts,
powsybl_case1354,
@@ -105,3 +106,24 @@ def test_parallel_pst_example_converges():
result_ac = run_ac(net)
assert result_ac[0].status_text == "Converged"
assert len(net.get_operational_limits())
+
+
+@pytest.mark.parametrize(
+ "linear_pst",
+ [
+ None,
+ [True, True, True, True],
+ [False, False, False, False],
+ [True, False, True, False],
+ [False, True, False, True],
+ [True, True, False, False],
+ [False, False, True, True],
+ ],
+)
+def test_grouped_pst_grid_example(linear_pst):
+ net = grouped_pst_grid_example(linear_pst=linear_pst)
+ result_dc = run_dc(net)
+ assert result_dc[0].status_text == "Converged"
+ result_ac = run_ac(net)
+ assert result_ac[0].status_text == "Converged"
+ assert len(net.get_operational_limits())
diff --git a/packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py b/packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
index 4477fc61a..4b804c19d 100644
--- a/packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
+++ b/packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
@@ -13,7 +13,7 @@
import pandera.typing as pat
import structlog
from beartype.typing import Literal, Optional, Union
-from jaxtyping import ArrayLike, Bool
+from jaxtyping import Bool
from toop_engine_importer.network_graph.data_classes import (
BranchSchema,
BusbarConnectionInfo,
@@ -732,9 +732,9 @@ def get_station_connection_tables(
def remove_double_connections(
- switching_table: Bool[ArrayLike, " n_bus n_asset"],
+ switching_table: Bool[np.ndarray, " n_bus n_asset"],
substation_id: Optional[str] = None,
-) -> Bool[ArrayLike, " n_bus n_asset"]:
+) -> Bool[np.ndarray, " n_bus n_asset"]:
"""Remove double connections from the switching table.
An Asset can be connected to multiple busbars.
diff --git a/packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py b/packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
index b4d604e7e..7e74bb493 100644
--- a/packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
+++ b/packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
@@ -75,6 +75,25 @@ def node_breaker_topology_to_graph_data(net: Network, substation_info: Substatio
NetworkGraphData.
"""
all_names_df = get_all_element_names(net, line_trafo_name_col="name")
+ branches_df = net.get_branches(attributes=["connected1", "connected2", "bus1_id", "bus2_id"])
+ injections_df = net.get_injections(attributes=["connected", "bus_id"])
+ buses_df = net.get_buses(attributes=["connected_component"])
+ in_main_connected_component = buses_df["connected_component"].fillna(0).eq(0)
+
+ asset_in_service = pd.concat(
+ [
+ (
+ branches_df["connected1"].fillna(False)
+ & branches_df["connected2"].fillna(False)
+ & branches_df["bus1_id"].map(in_main_connected_component).fillna(False)
+ & branches_df["bus2_id"].map(in_main_connected_component).fillna(False)
+ ).rename("in_service"),
+ (
+ injections_df["connected"].fillna(False)
+ & injections_df["bus_id"].map(in_main_connected_component).fillna(False)
+ ).rename("in_service"),
+ ]
+ )
nbt = net.get_node_breaker_topology(substation_info.voltage_level_id)
switches_df = get_switches(switches_df=nbt.switches)
@@ -86,7 +105,11 @@ def node_breaker_topology_to_graph_data(net: Network, substation_info: Substatio
substation_info=substation_info,
)
helper_branches = get_helper_branches(internal_connections_df=nbt.internal_connections)
- node_assets_df = get_node_assets(nodes_df=nodes_df, all_names_df=all_names_df)
+ node_assets_df = get_node_assets(
+ nodes_df=nodes_df,
+ all_names_df=all_names_df,
+ asset_in_service=asset_in_service,
+ )
graph_data = NetworkGraphData(
nodes=nodes_df,
@@ -232,7 +255,11 @@ def get_helper_branches(internal_connections_df: pd.DataFrame) -> pat.DataFrame[
return helper_branches
-def get_node_assets(nodes_df: pd.DataFrame, all_names_df: pd.Series) -> pat.DataFrame[NodeAssetSchema]:
+def get_node_assets(
+ nodes_df: pd.DataFrame,
+ all_names_df: pd.Series,
+ asset_in_service: pd.Series,
+) -> pat.DataFrame[NodeAssetSchema]:
"""Get node assets from a node breaker topology.
Get the node assets from a node breaker topology, rename and retype for the NetworkGraph.
@@ -243,6 +270,8 @@ def get_node_assets(nodes_df: pd.DataFrame, all_names_df: pd.Series) -> pat.Data
The nodes DataFrame from the node NodeBreakerTopology.
all_names_df : pd.Series
The names of all elements in the network.
+ asset_in_service : pd.Series
+ Boolean service state by connectable id derived from the Powsybl network model.
Returns
-------
@@ -260,8 +289,7 @@ def get_node_assets(nodes_df: pd.DataFrame, all_names_df: pd.Series) -> pat.Data
node_assets_df.rename(columns={"connectable_type": "asset_type", "name": "foreign_id"}, inplace=True)
node_assets_df.fillna({"foreign_id": ""}, inplace=True)
node_assets_df = node_assets_df[["grid_model_id", "foreign_id", "node", "asset_type"]]
- # TODO: might need to be changed once there is more information about the in_service state
- node_assets_df["in_service"] = True
+ node_assets_df["in_service"] = node_assets_df["grid_model_id"].map(asset_in_service).fillna(True).astype(bool)
return node_assets_df
@@ -402,6 +430,12 @@ def get_relevant_voltage_levels(network: Network, network_masks: NetworkMasks) -
voltage_levels = get_voltage_level_with_region(network, attributes=attributes)
busses = network.get_buses()
relevant_voltage_levels = busses[network_masks.relevant_subs]["voltage_level_id"]
+ busbar_sections = network.get_busbar_sections(attributes=["bus_id"])
+ busbar_outage_bus_ids = pd.Index(busbar_sections[network_masks.busbar_for_nminus1]["bus_id"].unique())
+ relevant_busbar_buses = busses.loc[busses.index.intersection(busbar_outage_bus_ids)]
+ relevant_voltage_levels = pd.concat(
+ [relevant_voltage_levels, relevant_busbar_buses["voltage_level_id"]]
+ ).drop_duplicates()
relevant_voltage_level_with_region = voltage_levels[voltage_levels.index.isin(relevant_voltage_levels)]
relevant_voltage_level_with_region_and_bus_id = relevant_voltage_level_with_region.merge(
relevant_voltage_levels, left_index=True, right_on="voltage_level_id", how="left"
diff --git a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/cgmes/powsybl_masks_cgmes.py b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/cgmes/powsybl_masks_cgmes.py
index 8c4f257a1..5e5b0bcfa 100644
--- a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/cgmes/powsybl_masks_cgmes.py
+++ b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/cgmes/powsybl_masks_cgmes.py
@@ -109,6 +109,10 @@ def get_most_connected_bus_at_voltage_level(
node_breaker_topology = net.get_node_breaker_topology(voltage_level_id)
switches = bus_breaker_topology.switches
elements = bus_breaker_topology.elements
+ elements.rename(columns={"bus_id": "bus_breaker_id"}, inplace=True)
+ elements = elements.merge(
+ net.get_bus_breaker_view_buses()[["bus_id"]], left_on="bus_breaker_id", right_index=True, how="left"
+ )
if switches[switches["kind"] == "BREAKER"].empty:
return None
diff --git a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
index c85aa8717..9e5ee689c 100644
--- a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
+++ b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
@@ -778,6 +778,7 @@ def update_bus_masks(
The updated network mask object including the bus masks.
"""
buses = network.get_buses()
+ busbar_sections = network.get_busbar_sections(attributes=["bus_id", "voltage_level_id"])
if importer_parameters.data_type == "ucte":
relevant_subs = buses.index.isin(
get_switchable_buses_ucte(
@@ -788,6 +789,11 @@ def update_bus_masks(
)
)
substation_ids = buses["voltage_level_id"].values
+ busbar_area_mask = get_mask_for_area_codes(
+ busbar_sections,
+ importer_parameters.area_settings.nminus1_area,
+ "voltage_level_id",
+ )
elif importer_parameters.data_type == "cgmes":
relevant_subs = buses.index.isin(
get_switchable_buses_cgmes(
@@ -799,15 +805,37 @@ def update_bus_masks(
)
)
substation_ids = buses["name"].str[:-2].values
+ busbar_sections = get_region_for_df(df=busbar_sections, network=network)
+ busbar_area_mask = get_mask_for_area_codes(
+ busbar_sections,
+ importer_parameters.area_settings.nminus1_area,
+ "region",
+ )
else:
raise ValueError(f"Data type {importer_parameters.data_type} is not supported.")
+ if importer_parameters.select_by_voltage_level_id_list is None:
+ busbar_hv_mask = (
+ get_voltage_from_voltage_level_id(network, busbar_sections["voltage_level_id"])
+ >= importer_parameters.area_settings.cutoff_voltage
+ )
+ busbar_for_nminus1 = busbar_hv_mask & busbar_area_mask
+ else:
+ busbar_for_nminus1 = (
+ busbar_sections["voltage_level_id"]
+ .isin(importer_parameters.select_by_voltage_level_id_list)
+ .to_numpy(dtype=bool)
+ )
+
blacklisted_substations = np.isin(substation_ids, blacklisted_ids)
relevant_subs = relevant_subs & ~blacklisted_substations
+ busbar_substation_ids = pd.Series(substation_ids, index=buses.index).reindex(busbar_sections["bus_id"]).to_numpy()
+ busbar_for_nminus1 = np.logical_and(busbar_for_nminus1, ~np.isin(busbar_substation_ids, blacklisted_ids))
return replace(
network_masks,
relevant_subs=relevant_subs,
+ busbar_for_nminus1=busbar_for_nminus1,
)
@@ -1088,6 +1116,8 @@ def make_masks(
if not validate_network_masks(network_masks, default_masks):
raise RuntimeError("Network masks are not created correctly.")
+ network_masks = remove_slack_busbar_sections(network_masks, network, slack_id=slack_id)
+
return network_masks
@@ -1171,9 +1201,30 @@ def remove_slack_from_relevant_subs(network_masks: NetworkMasks, network: Networ
network_masks: NetworkMasks
The updated network masks without the slack bus in the relevant_subs mask.
"""
- return replace(
- network_masks, relevant_subs=network_masks.relevant_subs & ~network.get_buses(attributes=[]).index.isin([slack_id])
- )
+ relevant_subs = network_masks.relevant_subs & ~network.get_buses(attributes=[]).index.isin([slack_id])
+ return replace(network_masks, relevant_subs=relevant_subs)
+
+
+def remove_slack_busbar_sections(network_masks: NetworkMasks, network: Network, slack_id: str) -> NetworkMasks:
+ """Remove busbar sections that belong to the slack bus from the N-1 mask.
+
+ Parameters
+ ----------
+ network_masks: NetworkMasks
+ The network masks to update.
+ network: Network
+ The network to get the busbar-section information from.
+ slack_id: str
+ The id of the slack bus.
+
+ Returns
+ -------
+ NetworkMasks
+ The updated network masks without slack-connected busbar sections in the N-1 mask.
+ """
+ busbar_sections = network.get_busbar_sections(attributes=["bus_id"])
+ busbar_for_nminus1 = network_masks.busbar_for_nminus1 & ~busbar_sections["bus_id"].isin([slack_id]).to_numpy()
+ return replace(network_masks, busbar_for_nminus1=busbar_for_nminus1)
def update_masks_from_power_factory_contingency_list_file(
@@ -1256,11 +1307,30 @@ def update_masks_from_power_factory_contingency_list_file(
)
load_nminus1_mask = load_hv_mask & network.get_loads().index.isin(grid_model_ids)
- busbar_df = network.get_busbar_sections(attributes=["voltage_level_id"])
- busbar_for_nminus1 = (
+ busbar_df = network.get_busbar_sections(attributes=["name", "bus_id", "voltage_level_id"])
+ busbar_hv_mask = (
get_voltage_from_voltage_level_id(network, busbar_df.voltage_level_id)
>= importer_parameters.area_settings.cutoff_voltage
- ) & busbar_df.index.isin(grid_model_ids)
+ )
+ busbar_rows = processed_n1_definition["element_type"].isin(["BUS", "BUSBAR_SECTION"])
+ if "power_factory_element_type" in processed_n1_definition:
+ busbar_rows |= processed_n1_definition["power_factory_element_type"].isin(["BUS", "BUSBAR_SECTION"])
+ busbar_candidate_ids = pd.unique(
+ pd.concat(
+ [
+ processed_n1_definition.loc[busbar_rows, "grid_model_id"],
+ processed_n1_definition.loc[busbar_rows, "power_factory_grid_model_rdf_id"],
+ processed_n1_definition.loc[busbar_rows, "power_factory_grid_model_fid"],
+ processed_n1_definition.loc[busbar_rows, "power_factory_grid_model_name"],
+ ],
+ ignore_index=True,
+ ).dropna()
+ )
+ busbar_for_nminus1 = busbar_hv_mask & (
+ busbar_df.index.isin(busbar_candidate_ids)
+ | busbar_df["bus_id"].isin(busbar_candidate_ids).to_numpy(dtype=bool)
+ | busbar_df["name"].isin(busbar_candidate_ids).to_numpy(dtype=bool)
+ )
if not process_multi_outages:
grid_model_ids = processed_n1_definition["grid_model_id"].unique()
diff --git a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
index 9a42e08e2..b4458a940 100644
--- a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
+++ b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
@@ -228,6 +228,14 @@ def create_nminus1_definition_from_masks(network: Network, network_masks: Networ
MonitoredElement(id=idx, name=row["name"], type="BUSBAR_SECTION", kind="bus")
for idx, row in busbar_sections[busbar_sections.index.isin(relevant_bus_ids)].iterrows()
]
+ outaged_busbars = [
+ Contingency(
+ id=idx,
+ name=row["name"],
+ elements=[GridElement(id=idx, name=row["name"], type="BUSBAR_SECTION", kind="bus")],
+ )
+ for idx, row in busbar_sections[network_masks.busbar_for_nminus1].iterrows()
+ ]
busbreaker_buses = network.get_bus_breaker_view_buses(attributes=["name", "bus_id"])
monitored_busbreakers = [
MonitoredElement(id=idx, name=row["name"], type="BUS_BREAKER_BUS", kind="bus")
@@ -254,6 +262,7 @@ def create_nminus1_definition_from_masks(network: Network, network_masks: Networ
+ outaged_generators
+ outaged_loads
+ outaged_switches
+ + outaged_busbars
),
)
return nminus1_definition
diff --git a/packages/importer_pkg/tests/network_graph/test_powsybl_station_to_graph.py b/packages/importer_pkg/tests/network_graph/test_powsybl_station_to_graph.py
index d7cf9ab21..606e4ad78 100644
--- a/packages/importer_pkg/tests/network_graph/test_powsybl_station_to_graph.py
+++ b/packages/importer_pkg/tests/network_graph/test_powsybl_station_to_graph.py
@@ -114,6 +114,16 @@ def test_get_helper_branches(basic_node_breaker_network_powsybl_grid):
def test_get_node_assets(basic_node_breaker_network_powsybl_grid):
net = basic_node_breaker_network_powsybl_grid
nbt = net.get_node_breaker_topology("VL1")
+ branches_df = net.get_branches(attributes=["connected1", "connected2"])
+ injections_df = net.get_injections(attributes=["connected"])
+ asset_in_service = pd.concat(
+ [
+ (branches_df["connected1"].fillna(False) & branches_df["connected2"].fillna(False)).rename("in_service"),
+ injections_df["connected"].fillna(False).rename("in_service"),
+ ]
+ )
+ asset_in_service.loc["L1"] = False
+ asset_in_service.loc["generator1"] = False
names_dict = {
"L1": "",
"L2": "",
@@ -143,8 +153,11 @@ def test_get_node_assets(basic_node_breaker_network_powsybl_grid):
switches_df=switches_df,
substation_info=substation_information,
)
- node_assets_df = get_node_assets(nodes_df=nodes_df, all_names_df=all_names_df)
- node_assets_df["in_service"] = True
+ node_assets_df = get_node_assets(nodes_df=nodes_df, all_names_df=all_names_df, asset_in_service=asset_in_service)
+
+ assert not node_assets_df.loc[node_assets_df["grid_model_id"] == "L1", "in_service"].item()
+ assert not node_assets_df.loc[node_assets_df["grid_model_id"] == "generator1", "in_service"].item()
+ assert node_assets_df.loc[node_assets_df["grid_model_id"] == "L2", "in_service"].item()
NodeAssetSchema.validate(node_assets_df)
@@ -617,20 +630,27 @@ def test_get_topo_integration(basic_node_breaker_network_powsybl_grid: Network):
blacklisted_ids=[],
)
relevant_voltage_level_with_region = get_relevant_voltage_levels(network=net, network_masks=network_masks)
- expected = ["VL1", "VL2", "VL3", "VL4", "VL5"]
- assert all(net.get_voltage_levels().index == expected)
- # VL4 has only 3 branches, VL5 has only 1 busbar
- assert all(relevant_voltage_level_with_region["voltage_level_id"] == expected[1:3])
- assert all(relevant_voltage_level_with_region.index == ["VL2_0", "VL3_0"])
+ # net.get_buses().index
+ # [
+ # VL1_0, slack -> not relevant
+ # VL2_0, relevant
+ # VL3_0, relevant
+ # VL4_0, RelevantStationRules() require per default 4 braches -> only 3 branches, but on busbaroutage list > relevant
+ # VL5_0, 1 branch, 1 injection, 1 busbar -> but on busbar outage list -> relevant
+ # ]
+ expected_relevant_voltage_level_with_region = ["VL2", "VL3", "VL4", "VL5"]
+ assert all(net.get_voltage_levels().index == ["VL1", "VL2", "VL3", "VL4", "VL5"])
+ assert all(relevant_voltage_level_with_region["voltage_level_id"] == expected_relevant_voltage_level_with_region)
+ assert all(relevant_voltage_level_with_region.index == ["VL2_0", "VL3_0", "VL4_0", "VL5_0"])
res = get_station_list(network=net, relevant_voltage_level_with_region=relevant_voltage_level_with_region)
- assert len(res) == 2
+ assert len(res) == 4
assert all([isinstance(station, Station) for station in res])
timestamp = datetime.datetime.now()
res = get_topology(network=net, network_masks=network_masks, importer_parameters=importer_parameters)
assert isinstance(res, Topology)
- assert len(res.stations) == 2
+ assert len(res.stations) == 4
assert res.topology_id == "cgmes_file.zip"
assert res.grid_model_file == "cgmes_file.zip"
assert res.timestamp - timestamp < datetime.timedelta(seconds=3)
@@ -701,7 +721,7 @@ def test_create_complex_grid_battery_hvdc_svc_3w_trafo_asset_topo():
),
)
- lf_result, *_ = pypowsybl.loadflow.run_dc(net)
+ lf_result, *_ = pypowsybl.loadflow.run_ac(net)
network_masks = powsybl_masks.make_masks(
network=net,
slack_id=lf_result.reference_bus_id,
@@ -709,25 +729,50 @@ def test_create_complex_grid_battery_hvdc_svc_3w_trafo_asset_topo():
blacklisted_ids=[],
)
relevant_voltage_level_with_region = get_relevant_voltage_levels(network=net, network_masks=network_masks)
+ # net.get_buses().index =
+ # [
+ # 'VL_3W_HV_0', large station
+ # 'VL_3W_MV_0', large station
+ # 'VL_3W_LV_0', 2 branches, 1 injections, but busbar relevant
+ # 'VL_2W_MV_LV_MV_0', 4 branches -> relevant
+ # 'VL_2W_MV_LV_LV_0', 2 branches, 1 injections, but busbar relevant
+ # 'VL_LV_load_0', 2 branches, 2 injections, 1 busbar, but busbar relevant
+ # 'VL_MV_load_0', 3 branches + pst -> 4 branches, 1 injection -> relevant
+ # 'VL_MV_load_3', other side of pst -> not relevant
+ # 'VL_MV_svc_0', large station
+ # 'VL_MV_0', large station
+ # 'VL_2W_MV_HV_MV_0', large station + pst
+ # 'VL_2W_MV_HV_MV_2', other side of pst -> not relevant
+ # 'VL_2W_MV_HV_HV_0', large station
+ # 'VL_HV_gen_0', large station, slack bus -> not relevant
+ # 'VL_HV_vsc_0', 4 branches, 1 injection, 1 HVDC
+ # '3W-Star-VL_0' not relevant
+ # ]
expected = [
"VL_3W_HV",
"VL_3W_MV",
+ "VL_3W_LV",
"VL_2W_MV_LV_MV",
+ "VL_2W_MV_LV_LV",
+ "VL_LV_load",
+ "VL_MV_load",
"VL_MV_svc",
"VL_MV",
"VL_2W_MV_HV_MV",
"VL_2W_MV_HV_HV",
"VL_HV_vsc",
- "VL_MV_load",
]
- # 'VL_HV_gen' not included as it is the slack
+ # 'VL_HV_gen_0' not included as it is the slack
for vl in expected:
assert vl in relevant_voltage_level_with_region["voltage_level_id"].values, f"Expected voltage level {vl} not found"
res = get_station_list(network=net, relevant_voltage_level_with_region=relevant_voltage_level_with_region)
- assert len(res) >= len(expected)
+ assert len(res) == len(expected)
assert all([isinstance(station, Station) for station in res])
+ station_names = [station.name for station in res]
+ for bus_id in expected:
+ assert bus_id in station_names, f"Expected station {bus_id} not found in station list"
res = get_topology(network=net, network_masks=network_masks, importer_parameters=importer_parameters)
assert isinstance(res, Topology)
diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_powsybl_masks.py b/packages/importer_pkg/tests/pypowsybl_import/test_powsybl_masks.py
index 583e98ba7..fdd8081ae 100644
--- a/packages/importer_pkg/tests/pypowsybl_import/test_powsybl_masks.py
+++ b/packages/importer_pkg/tests/pypowsybl_import/test_powsybl_masks.py
@@ -16,7 +16,7 @@
import pytest
from fsspec.implementations.local import LocalFileSystem
from pypowsybl.network import Network
-from toop_engine_grid_helpers.powsybl.example_grids import parallel_pst_example
+from toop_engine_grid_helpers.powsybl.example_grids import grouped_pst_grid_example, parallel_pst_example
from toop_engine_importer.pypowsybl_import import network_analysis, powsybl_masks, preprocessing
from toop_engine_importer.pypowsybl_import.data_classes import PreProcessingStatistics
from toop_engine_importer.pypowsybl_import.ucte.powsybl_masks_ucte import get_switchable_buses_ucte
@@ -542,10 +542,23 @@ def test_update_bus_masks(ucte_file_with_border, ucte_importer_parameters: UcteI
)
expected_bus_mask[3] = False
assert np.array_equal(updated_masks.relevant_subs, expected_bus_mask)
+ expected_busbar_mask = (
+ powsybl_masks.get_voltage_from_voltage_level_id(
+ network,
+ network.get_busbar_sections(attributes=["voltage_level_id"])["voltage_level_id"],
+ )
+ >= importer_parameters.area_settings.cutoff_voltage
+ ) & powsybl_masks.get_mask_for_area_codes(
+ network.get_busbar_sections(attributes=["voltage_level_id"]),
+ importer_parameters.area_settings.nminus1_area,
+ "voltage_level_id",
+ )
+ assert np.array_equal(network_masks.busbar_for_nminus1, expected_busbar_mask)
def test_update_bus_masks_node_breaker_select_station(basic_node_breaker_network_powsybl_grid: Network):
network = basic_node_breaker_network_powsybl_grid
+ buses = network.get_buses(attributes=[])
importer_parameters = CgmesImporterParameters(
grid_model_file=Path("cgmes_file.zip"),
data_folder="data_folder",
@@ -556,6 +569,19 @@ def test_update_bus_masks_node_breaker_select_station(basic_node_breaker_network
expected_bus_mask = np.array([True, True, True, False, False])
assert np.array_equal(updated_masks.relevant_subs, expected_bus_mask)
+ busbar_sections = powsybl_masks.get_region_for_df(
+ network=network,
+ df=network.get_busbar_sections(attributes=["voltage_level_id"]),
+ )
+ expected_busbar_mask = (
+ powsybl_masks.get_voltage_from_voltage_level_id(network, busbar_sections["voltage_level_id"])
+ >= importer_parameters.area_settings.cutoff_voltage
+ ) & powsybl_masks.get_mask_for_area_codes(
+ busbar_sections,
+ importer_parameters.area_settings.nminus1_area,
+ "region",
+ )
+ assert np.array_equal(updated_masks.busbar_for_nminus1, expected_busbar_mask)
# make sure the slack is removed from relevant subs
lf_result, *_ = pypowsybl.loadflow.run_dc(network)
@@ -567,6 +593,11 @@ def test_update_bus_masks_node_breaker_select_station(basic_node_breaker_network
)
expected_bus_mask_no_slack = np.array([False, True, True, False, False])
assert np.array_equal(network_masks.relevant_subs, expected_bus_mask_no_slack)
+ expected_busbar_mask_no_slack = (
+ expected_busbar_mask
+ & ~network.get_busbar_sections(attributes=["bus_id"])["bus_id"].isin([lf_result.reference_bus_id]).to_numpy()
+ )
+ assert np.array_equal(network_masks.busbar_for_nminus1, expected_busbar_mask_no_slack)
importer_parameters.select_by_voltage_level_id_list = list(network.get_voltage_levels().index)
updated_masks = powsybl_masks.update_bus_masks(default_masks, network, importer_parameters, blacklisted_ids=[])
@@ -708,12 +739,32 @@ def test_make_masks_node_breaker(
basic_node_breaker_network_powsybl_not_disconnectable, cgmes_importer_parameters: CgmesImporterParameters
):
network = basic_node_breaker_network_powsybl_not_disconnectable
+ buses = network.get_buses(attributes=[])
default_masks = powsybl_masks.create_default_network_masks(network)
lf_result, *_ = pypowsybl.loadflow.run_dc(network)
masks = powsybl_masks.make_masks(
network=network, slack_id=lf_result.reference_bus_id, importer_parameters=cgmes_importer_parameters
)
assert powsybl_masks.validate_network_masks(masks, default_masks)
+ busbar_sections = powsybl_masks.get_region_for_df(
+ network=network,
+ df=network.get_busbar_sections(attributes=["bus_id", "voltage_level_id"]),
+ )
+ expected_busbar_mask = (
+ powsybl_masks.get_voltage_from_voltage_level_id(network, busbar_sections["voltage_level_id"])
+ >= cgmes_importer_parameters.area_settings.cutoff_voltage
+ ) & powsybl_masks.get_mask_for_area_codes(
+ busbar_sections,
+ cgmes_importer_parameters.area_settings.nminus1_area,
+ "region",
+ )
+ expected_busbar_mask &= ~busbar_sections["bus_id"].isin([lf_result.reference_bus_id]).to_numpy()
+ assert np.array_equal(masks.busbar_for_nminus1, expected_busbar_mask)
+ assert (
+ not network.get_busbar_sections(attributes=["bus_id"])["bus_id"][masks.busbar_for_nminus1]
+ .isin([lf_result.reference_bus_id])
+ .any()
+ )
def test_make_masks_node_breaker_with_ignore_file(
@@ -804,6 +855,38 @@ def test_update_masks_from_contingency_list_file(
assert np.array_equal(network_masks.boundary_line_for_nminus1, np.array([False, False, True, False, False]))
+def test_update_masks_from_power_factory_contingency_file_with_busbar_ids(
+ basic_node_breaker_network_powsybl_grid: Network,
+ cgmes_importer_parameters: CgmesImporterParameters,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ network = basic_node_breaker_network_powsybl_grid
+ busbar_sections = network.get_busbar_sections(attributes=["bus_id", "voltage_level_id"])
+ selected_bus_id = str(busbar_sections.iloc[0]["bus_id"])
+
+ contingency_file = tmp_path_factory.mktemp("contingency_busbar_file") / "contingency.csv"
+ contingency_file.write_text(
+ """index;contingency_name;contingency_id;power_factory_grid_model_name;power_factory_grid_model_fid;power_factory_grid_model_rdf_id;power_factory_element_type
+1;busbar;1;{bus_id};{bus_id};{bus_id};BUS
+""".format(bus_id=selected_bus_id)
+ )
+ cgmes_importer_parameters.contingency_list_file = contingency_file
+
+ default_masks = powsybl_masks.create_default_network_masks(network)
+ network_masks = powsybl_masks.update_masks_from_power_factory_contingency_list_file(
+ network_masks=default_masks,
+ network=network,
+ importer_parameters=cgmes_importer_parameters,
+ filesystem=LocalFileSystem(),
+ )
+
+ expected_busbar_mask = (
+ powsybl_masks.get_voltage_from_voltage_level_id(network, busbar_sections["voltage_level_id"])
+ >= cgmes_importer_parameters.area_settings.cutoff_voltage
+ ) & busbar_sections["bus_id"].isin([selected_bus_id]).to_numpy()
+ assert np.array_equal(network_masks.busbar_for_nminus1, expected_busbar_mask)
+
+
def test_make_masks_with_contingency_file(
ucte_file_with_border, ucte_importer_parameters: UcteImporterParameters, tmp_path_factory: pytest.TempPathFactory
):
@@ -890,6 +973,64 @@ def test_build_pst_group_labels_marks_non_controllable_as_ungrouped():
assert label_by_id["PST3"] == -1
+@pytest.mark.parametrize(
+ ("linear_pst", "split_pst_station", "expected_group_count", "expected_grouped_pairs"),
+ [
+ (
+ [True, True, True, True],
+ False,
+ 1,
+ [("PST_1_group_1", "PST_2_group_1"), ("PST_3_group_2", "PST_4_group_2")],
+ ),
+ (
+ [True, True, True, True],
+ True,
+ 2,
+ [("PST_1_group_1", "PST_3_group_2"), ("PST_2_group_1", "PST_4_group_2")],
+ ),
+ (
+ [True, False, True, False],
+ False,
+ 2,
+ [("PST_1_group_1", "PST_3_group_2"), ("PST_2_group_1", "PST_4_group_2")],
+ ),
+ ],
+)
+def test_grouped_pst_grid_importer_masks_include_pst_groups(
+ linear_pst: list[bool],
+ split_pst_station: bool,
+ expected_group_count: int,
+ expected_grouped_pairs: list[tuple[str, str]],
+ cgmes_importer_parameters: CgmesImporterParameters,
+) -> None:
+ net = grouped_pst_grid_example(linear_pst=linear_pst)
+ if split_pst_station:
+ net.open_switch("VL2_BREAKER#0")
+ trafos = net.get_2_windings_transformers(attributes=[])
+ importer_parameters = deepcopy(cgmes_importer_parameters)
+ importer_parameters.area_settings.control_area = ["BE"]
+ importer_parameters.area_settings.view_area = ["BE"]
+ importer_parameters.area_settings.nminus1_area = ["BE"]
+ importer_parameters.area_settings.cutoff_voltage = 220
+
+ network_masks = powsybl_masks.make_masks(
+ network=net,
+ slack_id="VL1_0",
+ importer_parameters=importer_parameters,
+ blacklisted_ids=[],
+ )
+ label_by_id = dict(zip(trafos.index, network_masks.pst_group_labels, strict=True))
+
+ assert powsybl_masks.validate_network_masks(network_masks, powsybl_masks.create_default_network_masks(net))
+ if expected_group_count == 2:
+ assert set(label_by_id.values()) == {0, 1}
+ else:
+ assert set(label_by_id.values()) == {0}
+ assert len(set(network_masks.pst_group_labels)) == expected_group_count
+ for first_pst_id, second_pst_id in expected_grouped_pairs:
+ assert label_by_id[first_pst_id] == label_by_id[second_pst_id]
+
+
def test_update_reward_masks_to_include_border_branches(
ucte_file_with_border, ucte_importer_parameters: UcteImporterParameters
):
diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py
index 92a1e2561..1acb440cc 100644
--- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py
+++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py
@@ -371,3 +371,15 @@ def test_create_nminus1_definition_from_masks_basic(ucte_file):
assert switches.index[0] in contingency_ids # switch_for_nminus1
# BASECASE contingency should exist
assert "BASECASE" in contingency_ids
+
+
+def test_create_nminus1_definition_from_masks_busbars(basic_node_breaker_network_powsybl_grid: Network) -> None:
+ network = basic_node_breaker_network_powsybl_grid
+ masks = powsybl_masks.create_default_network_masks(network=network)
+ masks.busbar_for_nminus1[0] = True
+
+ nminus1_def = create_nminus1_definition_from_masks(network, masks)
+ contingency_ids = [contingency.id for contingency in nminus1_def.contingencies]
+ busbar_sections = network.get_busbar_sections(attributes=["name"])
+
+ assert busbar_sections.index[0] in contingency_ids
diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/optimizer.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/optimizer.py
index 7bb6d0421..694f958af 100644
--- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/optimizer.py
+++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/optimizer.py
@@ -359,6 +359,7 @@ def loadflow_ref(loadflow: StoredLoadflowReference) -> LoadflowResultsPolars:
reject_critical_branch_threshold=ga_config.reject_critical_branch_threshold,
reject_voltage_jump_threshold=ga_config.reject_voltage_jump_threshold,
reject_critical_va_diff_threshold=ga_config.reject_critical_va_diff_threshold,
+ enable_critical_voltage_rejection=ga_config.enable_critical_voltage_rejection,
critical_voltage_jump_percent=ga_config.critical_voltage_jump_percent,
max_allowed_va_diff=ga_config.critical_va_diff_degree,
)
diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py
index 5c4a91d8c..8b6e7270a 100644
--- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py
+++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py
@@ -107,7 +107,7 @@ def compute_loadflow_and_metrics(
base_case_id: Optional[str],
cases_subset: Optional[Collection[str]] = None,
critical_voltage_jump_percent: float = 5.0,
- max_allowed_va_diff: float = 0.0,
+ max_allowed_va_diff: float = 20.0,
) -> tuple[LoadflowResultsPolars, Optional[AdditionalActionInfo], Metrics]:
"""Compute loadflow results and associated metrics for a given set of strategies.
@@ -192,7 +192,7 @@ def compute_metrics_single_timestep(
additional_info: Optional[AdditionalActionInfo],
base_case_id: Optional[str] = None,
critical_voltage_jump_percent: float = 5.0,
- max_allowed_va_diff: float = 0.0,
+ max_allowed_va_diff: float = 20.0,
) -> Metrics:
"""Compute the metrics for a single timestep
@@ -297,6 +297,7 @@ def evaluate_acceptance(
reject_critical_branch_threshold: float = 1.1,
reject_voltage_jump_threshold: float = 1.1,
reject_critical_va_diff_threshold: float = 1.1,
+ enable_critical_voltage_rejection: bool = False,
early_stopping: bool = False,
) -> Optional[TopologyRejectionReason]:
"""Evaluate if the split loadflow results are acceptable compared to the unsplit results.
@@ -337,6 +338,8 @@ def evaluate_acceptance(
reject_critical_va_diff_threshold : float, optional
The threshold for the critical voltage-angle-difference count increase, by default 1.1
(i.e. the split case must not have more than 110 % of the critical voltage-angle differences in the unsplit case).
+ enable_critical_voltage_rejection : bool, optional
+ Whether to reject based on the critical jump or voltage-angle-difference count increase.
early_stopping : bool, optional
Whether the acceptance is computed as part of an early stopping criterion, will set the early_stopping field in the
TopologyRejectionReason
@@ -395,33 +398,36 @@ def evaluate_acceptance(
early_stopping=early_stopping,
)
- unsplit_voltage_jumps = np.array([metrics_unsplit.extra_scores.get("voltage_jump_count_n_1", 999)], dtype=float)
- split_voltage_jumps = np.array([metrics_split.extra_scores.get("voltage_jump_count_n_1", 0)], dtype=float)
- voltage_jumps_acceptable = np.all(split_voltage_jumps <= unsplit_voltage_jumps * reject_voltage_jump_threshold)
- if not voltage_jumps_acceptable:
- return TopologyRejectionReason(
- criterion="voltage-magnitude",
- value_after=float(split_voltage_jumps.sum()),
- value_before=float(unsplit_voltage_jumps.sum()),
- threshold=reject_voltage_jump_threshold,
- early_stopping=early_stopping,
- description="Critical voltage jump count increased too much.",
- )
+ if enable_critical_voltage_rejection:
+ unsplit_voltage_jumps = np.array([metrics_unsplit.extra_scores.get("voltage_jump_count_n_1", 999)], dtype=float)
+ split_voltage_jumps = np.array([metrics_split.extra_scores.get("voltage_jump_count_n_1", 0)], dtype=float)
+ voltage_jumps_acceptable = np.all(split_voltage_jumps <= unsplit_voltage_jumps * reject_voltage_jump_threshold)
+ if not voltage_jumps_acceptable:
+ return TopologyRejectionReason(
+ criterion="voltage-magnitude",
+ value_after=float(split_voltage_jumps.sum()),
+ value_before=float(unsplit_voltage_jumps.sum()),
+ threshold=reject_voltage_jump_threshold,
+ early_stopping=early_stopping,
+ description="Critical voltage jump count increased too much.",
+ )
- unsplit_critical_va_diff = np.array([metrics_unsplit.extra_scores.get("critical_va_diff_count_n_1", 999)], dtype=float)
- split_critical_va_diff = np.array([metrics_split.extra_scores.get("critical_va_diff_count_n_1", 0)], dtype=float)
- critical_va_diff_acceptable = np.all(
- split_critical_va_diff <= unsplit_critical_va_diff * reject_critical_va_diff_threshold
- )
- if not critical_va_diff_acceptable:
- return TopologyRejectionReason(
- criterion="voltage-angle",
- value_after=float(split_critical_va_diff.sum()),
- value_before=float(unsplit_critical_va_diff.sum()),
- threshold=reject_critical_va_diff_threshold,
- early_stopping=early_stopping,
- description="Critical voltage-angle-difference count increased too much.",
+ unsplit_critical_va_diff = np.array(
+ [metrics_unsplit.extra_scores.get("critical_va_diff_count_n_1", 999)], dtype=float
+ )
+ split_critical_va_diff = np.array([metrics_split.extra_scores.get("critical_va_diff_count_n_1", 0)], dtype=float)
+ critical_va_diff_acceptable = np.all(
+ split_critical_va_diff <= unsplit_critical_va_diff * reject_critical_va_diff_threshold
)
+ if not critical_va_diff_acceptable:
+ return TopologyRejectionReason(
+ criterion="voltage-angle",
+ value_after=float(split_critical_va_diff.sum()),
+ value_before=float(unsplit_critical_va_diff.sum()),
+ threshold=reject_critical_va_diff_threshold,
+ early_stopping=early_stopping,
+ description="Critical voltage-angle-difference count increased too much.",
+ )
return None
@@ -432,6 +438,8 @@ def compute_remaining_loadflows(
base_case_id: Optional[str],
loadflows_subset: LoadflowResultsPolars,
cases_subset: list[str],
+ critical_voltage_jump_percent: float = 5.0,
+ max_allowed_va_diff: float = 20.0,
) -> tuple[LoadflowResultsPolars, Metrics]:
"""Compute the loadflows for the remaining contingencies that were not included in the early stopping subset.
@@ -452,6 +460,10 @@ def compute_remaining_loadflows(
cases_subset : list[str]
The contingency case ids that were included in the early stopping subset for each timestep. This could be extracted
from the loadflows_subset but as it is available it is faster to pass it in.
+ critical_voltage_jump_percent : float, optional
+ Voltage jumps larger than this percentage are counted as critical in the AC metrics.
+ max_allowed_va_diff : float, optional
+ Voltage angle differences larger than this value in degrees are counted as critical in the AC metrics.
Returns
-------
@@ -485,6 +497,8 @@ def compute_remaining_loadflows(
loadflow=lfs,
additional_info=additional_info_remaining,
base_case_id=base_case_id,
+ critical_voltage_jump_percent=critical_voltage_jump_percent,
+ max_allowed_va_diff=max_allowed_va_diff,
)
# Restore the original N-1 definitions in the runners
@@ -520,6 +534,9 @@ class ACScoringParameters:
reject_critical_va_diff_threshold: float
"""The rejection threshold for the critical voltage-angle-difference count increase."""
+ enable_critical_voltage_rejection: bool
+ """Whether to reject based on the critical voltage jump or voltage-angle-difference count increase."""
+
critical_voltage_jump_percent: float
"""Voltage jumps larger than this percentage are counted as critical in the AC metrics."""
@@ -638,6 +655,7 @@ def score_strategy_worst_k(
reject_critical_branch_threshold=scoring_params.reject_critical_branch_threshold,
reject_voltage_jump_threshold=scoring_params.reject_voltage_jump_threshold,
reject_critical_va_diff_threshold=scoring_params.reject_critical_va_diff_threshold,
+ enable_critical_voltage_rejection=scoring_params.enable_critical_voltage_rejection,
early_stopping=True,
)
return EarlyStoppingStageResult(
@@ -662,6 +680,7 @@ def score_strategy_worst_k(
reject_critical_branch_threshold=scoring_params.reject_critical_branch_threshold,
reject_voltage_jump_threshold=scoring_params.reject_voltage_jump_threshold,
reject_critical_va_diff_threshold=scoring_params.reject_critical_va_diff_threshold,
+ enable_critical_voltage_rejection=scoring_params.enable_critical_voltage_rejection,
early_stopping=False,
)
return EarlyStoppingStageResult(
@@ -781,6 +800,8 @@ def score_topology_remaining(
base_case_id=scoring_params.base_case_id,
loadflows_subset=early_stage_result.loadflow_results,
cases_subset=early_stage_result.cases_subset,
+ critical_voltage_jump_percent=scoring_params.critical_voltage_jump_percent,
+ max_allowed_va_diff=scoring_params.max_allowed_va_diff,
)
else:
lfs = early_stage_result.loadflow_results
@@ -794,6 +815,7 @@ def score_topology_remaining(
reject_critical_branch_threshold=scoring_params.reject_critical_branch_threshold,
reject_voltage_jump_threshold=scoring_params.reject_voltage_jump_threshold,
reject_critical_va_diff_threshold=scoring_params.reject_critical_va_diff_threshold,
+ enable_critical_voltage_rejection=scoring_params.enable_critical_voltage_rejection,
early_stopping=False,
)
return TopologyScoringResult(loadflow_results=lfs, metrics=metrics, rejection_reason=rejection_reason)
@@ -827,6 +849,8 @@ def score_strategy_full(
runner=runner,
topology=topology,
base_case_id=scoring_params.base_case_id,
+ critical_voltage_jump_percent=scoring_params.critical_voltage_jump_percent,
+ max_allowed_va_diff=scoring_params.max_allowed_va_diff,
)
rejection_reason = evaluate_acceptance(
metrics_split=metrics,
@@ -834,6 +858,9 @@ def score_strategy_full(
reject_convergence_threshold=scoring_params.reject_convergence_threshold,
reject_overload_threshold=scoring_params.reject_overload_threshold,
reject_critical_branch_threshold=scoring_params.reject_critical_branch_threshold,
+ reject_voltage_jump_threshold=scoring_params.reject_voltage_jump_threshold,
+ reject_critical_va_diff_threshold=scoring_params.reject_critical_va_diff_threshold,
+ enable_critical_voltage_rejection=scoring_params.enable_critical_voltage_rejection,
early_stopping=False,
)
return TopologyScoringResult(loadflow_results=lfs, metrics=metrics, rejection_reason=rejection_reason)
diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/dc/genetic_functions/initialization.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/dc/genetic_functions/initialization.py
index 66ec124c7..121321ad9 100644
--- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/dc/genetic_functions/initialization.py
+++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/dc/genetic_functions/initialization.py
@@ -461,14 +461,34 @@ def update_single_pair_bb_outage_information(
needs_penalty_baseline = (
should_enable_bb_outage and not bb_outage_as_nminus1 and has_rel_bb_outage_data and has_monitored_branches
)
- # Keep or create a baseline only when busbar outages stay enabled for this run.
- should_keep_or_create_baseline = should_enable_bb_outage and (has_stored_bb_outage_baseline or needs_penalty_baseline)
+ # Keep or create a baseline only in penalty mode. In N-1 mode, baseline must not influence case counting.
+ should_keep_or_create_baseline = (
+ should_enable_bb_outage and not bb_outage_as_nminus1 and (has_stored_bb_outage_baseline or needs_penalty_baseline)
+ )
+
+ base_nminus1_cases = (
+ dynamic_information.n_outages + dynamic_information.n_multi_outages + dynamic_information.n_inj_failures
+ )
+ expected_nminus1_cases = (
+ base_nminus1_cases + dynamic_information.n_bb_outages
+ if should_enable_bb_outage and bb_outage_as_nminus1
+ else base_nminus1_cases
+ )
+ contingency_ids = list(solver_config.contingency_ids)
+ if len(contingency_ids) < expected_nminus1_cases:
+ if len(contingency_ids) < base_nminus1_cases:
+ contingency_ids.extend(f"nminus1_case_{i}" for i in range(len(contingency_ids), base_nminus1_cases))
+ contingency_ids.extend(
+ f"bb_outage_case_{i}"
+ for i in range(len(contingency_ids) - base_nminus1_cases, expected_nminus1_cases - base_nminus1_cases)
+ )
updated_solver_config = replace(
solver_config,
enable_bb_outages=should_enable_bb_outage,
bb_outage_as_nminus1=bb_outage_as_nminus1,
clip_bb_outage_penalty=clip_bb_outage_penalty,
+ contingency_ids=contingency_ids,
)
updated_action_set = (
dynamic_information.action_set
diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/interfaces/messages/ac_params.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/interfaces/messages/ac_params.py
index dd3635f1b..78c9221b6 100644
--- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/interfaces/messages/ac_params.py
+++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/interfaces/messages/ac_params.py
@@ -76,6 +76,12 @@ class ACGAParameters(BaseModel):
"""The rejection threshold for the critical voltage-angle-difference count increase, i.e. the split case must have
less than 10% more critical voltage-angle differences than the unsplit case or it will be rejected."""
+ enable_critical_voltage_rejection: bool = False
+ """Whether to use critical voltage jumps and voltage-angle-difference counts as an acceptance/rejection criterion.
+
+ The associated metrics are still computed and reported when this flag is disabled.
+ """
+
critical_voltage_jump_percent: float = 5.0
"""Voltage jumps larger than this percentage are counted as critical in the AC metrics."""
diff --git a/packages/topology_optimizer_pkg/tests/ac/test_ac_scoring_functions.py b/packages/topology_optimizer_pkg/tests/ac/test_ac_scoring_functions.py
index ace1b2241..6aee3b7e4 100644
--- a/packages/topology_optimizer_pkg/tests/ac/test_ac_scoring_functions.py
+++ b/packages/topology_optimizer_pkg/tests/ac/test_ac_scoring_functions.py
@@ -26,6 +26,7 @@
evaluate_acceptance,
extract_switching_distance,
score_remaining_contingency_batch,
+ score_strategy_full,
score_strategy_worst_k_batch,
score_topology_batch,
)
@@ -83,6 +84,7 @@ def fake_worst_k(topology, runner, loadflow_results_unsplit, metrics_unsplit, sc
reject_critical_branch_threshold=1.1,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
critical_voltage_jump_percent=5.0,
max_allowed_va_diff=0.0,
base_case_id=None,
@@ -172,6 +174,7 @@ def fake_remaining(topology, runner, metrics_unsplit, scoring_params, early_stag
reject_critical_branch_threshold=1.1,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
critical_voltage_jump_percent=5.0,
max_allowed_va_diff=0.0,
base_case_id=None,
@@ -247,6 +250,7 @@ def fail_worst_k_batch(*args, **kwargs):
reject_critical_branch_threshold=1.1,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
critical_voltage_jump_percent=5.0,
max_allowed_va_diff=0.0,
base_case_id=None,
@@ -406,6 +410,7 @@ def test_evaluate_acceptance_identical_metrics():
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.0,
reject_critical_va_diff_threshold=1.0,
+ enable_critical_voltage_rejection=True,
)
assert reason is None, "Results rejected although they are the same as before and thresholds is exactly 1."
# Not accepted if any thresholds < 1.
@@ -417,6 +422,7 @@ def test_evaluate_acceptance_identical_metrics():
reject_critical_branch_threshold=0.9,
reject_voltage_jump_threshold=0.9,
reject_critical_va_diff_threshold=0.9,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results rejected although they are the same as before and thresholds is below 1."
assert reason.criterion == "convergence"
@@ -429,6 +435,7 @@ def test_evaluate_acceptance_identical_metrics():
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.0,
reject_critical_va_diff_threshold=1.0,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results rejected although they are just as good and convergence thresholds below 1."
assert reason.criterion == "convergence"
@@ -440,6 +447,7 @@ def test_evaluate_acceptance_identical_metrics():
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.0,
reject_critical_va_diff_threshold=1.0,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results rejected although they are just as good and overload thresholds below 1."
assert reason.criterion == "overload-energy"
@@ -452,6 +460,7 @@ def test_evaluate_acceptance_identical_metrics():
reject_critical_branch_threshold=0.9,
reject_voltage_jump_threshold=1.0,
reject_critical_va_diff_threshold=1.0,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results rejected although they are just as good and crit branch thresholds below 1."
assert reason.criterion == "critical-branch-count"
@@ -465,6 +474,7 @@ def test_evaluate_acceptance_identical_metrics():
reject_critical_branch_threshold=1.1,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
)
assert reason is None, "Results rejected although they are just as good and thresholds above 1."
@@ -500,6 +510,7 @@ def test_evaluate_acceptance_improved_metrics():
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.0,
reject_critical_va_diff_threshold=1.0,
+ enable_critical_voltage_rejection=True,
)
assert reason is None, "Results rejected although they are the same as before and thresholds is exactly 1."
# Accepted if all thresholds=0.9.
@@ -511,6 +522,7 @@ def test_evaluate_acceptance_improved_metrics():
reject_critical_branch_threshold=0.9,
reject_voltage_jump_threshold=0.9,
reject_critical_va_diff_threshold=0.9,
+ enable_critical_voltage_rejection=True,
)
assert reason is None, "Results not accepted although they improved by exactly 10 percent and thresholds is 0.9."
@@ -534,6 +546,7 @@ def test_evaluate_acceptance_improved_metrics():
reject_critical_branch_threshold=0.8,
reject_voltage_jump_threshold=0.8,
reject_critical_va_diff_threshold=0.8,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results accepted although they only improved by exactly 10 percent and thresholds is 0.8."
assert reason.criterion == "convergence"
@@ -547,6 +560,7 @@ def test_evaluate_acceptance_improved_metrics():
reject_critical_branch_threshold=1.1,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
)
assert reason is None, "Results rejected although they are just as good and thresholds above 1."
@@ -582,6 +596,7 @@ def test_evaluate_acceptance_worse_metrics():
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.0,
reject_critical_va_diff_threshold=1.0,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results accepted although they are worse as before and thresholds is exactly 1."
assert reason.criterion == "convergence"
@@ -595,6 +610,7 @@ def test_evaluate_acceptance_worse_metrics():
reject_critical_branch_threshold=0.9,
reject_voltage_jump_threshold=0.9,
reject_critical_va_diff_threshold=0.9,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None, "Results accepted although they got worse by exactly 10 percent and thresholds is 0.9."
assert reason.criterion == "convergence"
@@ -607,6 +623,7 @@ def test_evaluate_acceptance_worse_metrics():
reject_critical_branch_threshold=1.1,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
)
assert reason is None, "Results not accepted although they only got worse by exactly 10 percent and thresholds is 1.1."
@@ -641,6 +658,7 @@ def test_evaluate_acceptance_rejects_voltage_jump_increase() -> None:
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None
@@ -677,12 +695,118 @@ def test_evaluate_acceptance_rejects_critical_va_diff_increase() -> None:
reject_critical_branch_threshold=1.0,
reject_voltage_jump_threshold=1.1,
reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
)
assert reason is not None
assert reason.criterion == "voltage-angle"
+def test_evaluate_acceptance_zero_baseline_va_diff_respects_toggle() -> None:
+ metrics_unsplit = Metrics(
+ fitness=-1.0,
+ extra_scores={
+ "non_converging_loadflows": 1,
+ "overload_energy_n_1": 10.0,
+ "critical_branch_count_n_1": 1,
+ "voltage_jump_count_n_1": 1,
+ "critical_va_diff_count_n_1": 0,
+ },
+ )
+ metrics_split = Metrics(
+ fitness=-1.0,
+ extra_scores={
+ "non_converging_loadflows": 1,
+ "overload_energy_n_1": 10.0,
+ "critical_branch_count_n_1": 1,
+ "voltage_jump_count_n_1": 1,
+ "critical_va_diff_count_n_1": 1,
+ },
+ )
+
+ accepted_reason = evaluate_acceptance(
+ metrics_split=metrics_split,
+ metrics_unsplit=metrics_unsplit,
+ reject_convergence_threshold=1.0,
+ reject_overload_threshold=1.0,
+ reject_critical_branch_threshold=1.0,
+ reject_voltage_jump_threshold=1.0,
+ reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=False,
+ )
+
+ rejected_reason = evaluate_acceptance(
+ metrics_split=metrics_split,
+ metrics_unsplit=metrics_unsplit,
+ reject_convergence_threshold=1.0,
+ reject_overload_threshold=1.0,
+ reject_critical_branch_threshold=1.0,
+ reject_voltage_jump_threshold=1.0,
+ reject_critical_va_diff_threshold=1.1,
+ enable_critical_voltage_rejection=True,
+ )
+
+ assert accepted_reason is None
+ assert rejected_reason is not None
+ assert rejected_reason.criterion == "voltage-angle"
+
+
+def test_score_strategy_full_forwards_thresholds_and_toggle(monkeypatch: pytest.MonkeyPatch) -> None:
+ topology = ACOptimTopology(
+ actions=[1],
+ disconnections=[],
+ pst_setpoints=None,
+ unsplit=False,
+ timestep=0,
+ strategy_hash=b"full-path",
+ optimization_id="test",
+ optimizer_type=OptimizerType.AC,
+ fitness=0.0,
+ metrics={},
+ worst_k_contingency_cases=["c1"],
+ )
+ scoring_params = ACScoringParameters(
+ reject_convergence_threshold=1.0,
+ reject_overload_threshold=0.95,
+ reject_critical_branch_threshold=1.1,
+ reject_voltage_jump_threshold=0.7,
+ reject_critical_va_diff_threshold=0.85,
+ enable_critical_voltage_rejection=True,
+ critical_voltage_jump_percent=7.5,
+ max_allowed_va_diff=12.0,
+ base_case_id="BASECASE",
+ early_stop_validation=False,
+ )
+
+ split_metrics = Metrics(fitness=1.0, extra_scores={"overload_energy_n_1": 1.0})
+ unsplit_metrics = Metrics(fitness=1.0, extra_scores={"overload_energy_n_1": 1.0})
+
+ def fake_compute_loadflow_and_metrics(**kwargs):
+ assert kwargs["critical_voltage_jump_percent"] == 7.5
+ assert kwargs["max_allowed_va_diff"] == 12.0
+ return Mock(spec=LoadflowResultsPolars), None, split_metrics
+
+ def fake_evaluate_acceptance(**kwargs):
+ assert kwargs["reject_voltage_jump_threshold"] == 0.7
+ assert kwargs["reject_critical_va_diff_threshold"] == 0.85
+ assert kwargs["enable_critical_voltage_rejection"] is True
+
+ monkeypatch.setattr(
+ "toop_engine_topology_optimizer.ac.scoring_functions.compute_loadflow_and_metrics",
+ fake_compute_loadflow_and_metrics,
+ )
+ monkeypatch.setattr("toop_engine_topology_optimizer.ac.scoring_functions.evaluate_acceptance", fake_evaluate_acceptance)
+
+ result = score_strategy_full(
+ topology=topology,
+ runner=Mock(spec=AbstractLoadflowRunner),
+ metrics_unsplit=unsplit_metrics,
+ scoring_params=scoring_params,
+ )
+
+ assert result.metrics == split_metrics
+
+
def test_compute_remaining_loadflows(grid_folder: Path) -> None:
"""Test the compute_remaining_loadflows function.
diff --git a/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py b/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py
index 4c81cf2fe..1358d5b28 100644
--- a/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py
+++ b/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py
@@ -11,6 +11,7 @@
import pytest
from omegaconf import DictConfig
+from toop_engine_grid_helpers.powsybl.example_grids import basic_node_breaker_network_powsybl
from toop_engine_interfaces.messages.preprocess.preprocess_commands import (
CgmesImporterParameters,
PreprocessParameters,
@@ -21,9 +22,11 @@
get_paths,
prepare_importer_parameters,
run_pipeline,
+ run_preprocessing,
run_task_process,
set_environment_variables,
)
+from toop_engine_topology_optimizer.dc.genetic_functions.initialization import update_static_information
def test_run_task_process_no_conn(dc_config):
@@ -225,6 +228,97 @@ def _get_serialized_topology_fitness(topology: dict) -> float:
assert ac_metrics["dc_info"]["disconnections"] == (expected_topology.get("disconnections") or [])
+def test_run_task_process_with_imported_busbar_outages(tmp_path: Path) -> None:
+ input_grid = tmp_path / "grid.xiidm"
+ net = basic_node_breaker_network_powsybl()
+ # create a busbar outage related overload for L2 when the second busbar in VL2 is outaged
+ open_switches = ["load1_DISCONNECTOR_18_0", "L71_DISCONNECTOR_10_1"]
+ close_switches = ["load1_DISCONNECTOR_18_1", "L71_DISCONNECTOR_10_0"]
+ for switch in close_switches:
+ net.close_switch(switch)
+ for switch in open_switches:
+ net.open_switch(switch)
+
+ net.save(input_grid)
+
+ data_folder = tmp_path / input_grid.stem
+ importer_parameters = prepare_importer_parameters(input_grid, data_folder)
+ preprocessing_parameters = PreprocessParameters(action_set_clip=2**10, preprocess_bb_outages=True)
+
+ _, static_information = run_preprocessing(
+ importer_parameters=importer_parameters,
+ data_folder=data_folder,
+ preprocessing_parameters=preprocessing_parameters,
+ is_pandapower_net=False,
+ )
+
+ assert (
+ static_information.dynamic_information.action_set.rel_bb_outage_data is not None
+ or static_information.dynamic_information.non_rel_bb_outage_data is not None
+ or static_information.dynamic_information.bb_outage_baseline_analysis is not None
+ )
+ n_worst_contingencies = min(20, int(static_information.dynamic_information.n_nminus1_cases))
+ assert n_worst_contingencies > 0
+
+ dc_config = DictConfig(
+ {
+ "task_name": "test_busbar_outage_optimizer",
+ "fixed_files": [str(data_folder / "static_information.hdf5")],
+ "double_precision": None,
+ "tensorboard_dir": str(tmp_path / "results" / "{task_name}"),
+ "stats_dir": str(tmp_path / "results" / "{task_name}"),
+ "summary_frequency": None,
+ "checkpoint_frequency": None,
+ "stdout": None,
+ "double_limits": None,
+ "num_cuda_devices": 1,
+ "omp_num_threads": 1,
+ "xla_force_host_platform_device_count": None,
+ "output_json": str(tmp_path / "results" / "output.json"),
+ "lf_config": {"distributed": False},
+ "ga_config": {
+ "runtime_seconds": 5,
+ "enable_bb_outage": True,
+ "bb_outage_as_nminus1": True,
+ "n_worst_contingencies": n_worst_contingencies,
+ "target_metrics": [["overload_energy_n_1", 1.0]],
+ "me_descriptors": [{"metric": "split_subs", "num_cells": 5}],
+ "observed_metrics": ["overload_energy_n_1", "split_subs"],
+ },
+ }
+ )
+
+ updated_static_information = update_static_information(
+ static_informations=(static_information,),
+ batch_size=8,
+ enable_nodal_inj_optim=False,
+ enable_parallel_pst_group_optim=False,
+ enable_bb_outage=True,
+ bb_outage_as_nminus1=True,
+ clip_bb_outage_penalty=False,
+ bb_outage_more_islands_penalty=0.0,
+ )[0]
+ assert updated_static_information.solver_config.enable_bb_outages
+ # assert updated_static_information.solver_config.bb_outage_as_nminus1
+
+ set_environment_variables(dc_config)
+ result = run_task_process(dc_config)
+
+ assert result is not None
+ assert "overload_energy_n_1" in result["initial_metrics"]
+ assert result["initial_metrics"]["overload_energy_n_1"] > 0
+ assert result["max_fitness"] > result["initial_fitness"]
+ assert len(result["best_topos"]) > 0
+ best_topology = max(result["best_topos"], key=lambda topo: topo["metrics"]["fitness"])
+ assert best_topology["metrics"]["fitness"] > result["initial_fitness"]
+ assert (
+ best_topology["metrics"]["extra_scores"]["overload_energy_n_1"] <= result["initial_metrics"]["overload_energy_n_1"]
+ )
+ result_dir = Path(dc_config["output_json"]).parent
+ assert result_dir.exists()
+ assert len(list(result_dir.iterdir())) > 0
+
+
def test_run_pipeline_no_optimization_stage(pipeline_and_configs, preprocessing_parameters):
pipeline_cfg, dc_cfg, ac_cfg = pipeline_and_configs
pipeline_cfg = PipelineConfig(**pipeline_cfg)
diff --git a/packages/topology_optimizer_pkg/tests/conftest.py b/packages/topology_optimizer_pkg/tests/conftest.py
index 73f19bb87..e74b826de 100644
--- a/packages/topology_optimizer_pkg/tests/conftest.py
+++ b/packages/topology_optimizer_pkg/tests/conftest.py
@@ -37,6 +37,9 @@
oberrhein_data,
three_node_pst_example_folder_powsybl,
)
+from toop_engine_dc_solver.example_grids import (
+ grouped_pst_grid_example_data_folder as create_grouped_pst_grid_example_data_folder,
+)
from toop_engine_dc_solver.jax.types import ActionSet, StaticInformation
from toop_engine_dc_solver.preprocess import load_grid
from toop_engine_dc_solver.preprocess.network_data import NetworkData
@@ -316,6 +319,36 @@ def case57_non_converging_path(_case57_non_converging_path: Path, tmp_path: Path
return tmp_path
+@pytest.fixture(scope="session")
+def _grouped_pst_grid_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ """Preprocessed grouped PST grid prepared once per test session."""
+ tmp_path = tmp_path_factory.mktemp("grouped_pst_grid")
+ create_grouped_pst_grid_example_data_folder(tmp_path)
+ return tmp_path
+
+
+@pytest.fixture(scope="function")
+def grouped_pst_grid_path(_grouped_pst_grid_path: Path, tmp_path: Path) -> Path:
+ """Path to a per-test copy of the grouped PST grid."""
+ shutil.copytree(_grouped_pst_grid_path, tmp_path, dirs_exist_ok=True)
+ return tmp_path
+
+
+@pytest.fixture(scope="session")
+def _grouped_pst_grid_split_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ """Preprocessed grouped PST grid with the PST station switch split prepared once per test session."""
+ tmp_path = tmp_path_factory.mktemp("grouped_pst_grid_split")
+ create_grouped_pst_grid_example_data_folder(tmp_path, split_group_station=True)
+ return tmp_path
+
+
+@pytest.fixture(scope="function")
+def grouped_pst_grid_split_path(_grouped_pst_grid_split_path: Path, tmp_path: Path) -> Path:
+ """Path to a per-test copy of the grouped PST grid with the PST station switch split."""
+ shutil.copytree(_grouped_pst_grid_split_path, tmp_path, dirs_exist_ok=True)
+ return tmp_path
+
+
@pytest.fixture(scope="session")
def static_information_file(_grid_folder: Path) -> Path:
return _grid_folder / "oberrhein" / PREPROCESSING_PATHS["static_information_file_path"]
diff --git a/packages/topology_optimizer_pkg/tests/dc/genetic_functions/test_initialization.py b/packages/topology_optimizer_pkg/tests/dc/genetic_functions/test_initialization.py
index c312d639e..f6c751296 100644
--- a/packages/topology_optimizer_pkg/tests/dc/genetic_functions/test_initialization.py
+++ b/packages/topology_optimizer_pkg/tests/dc/genetic_functions/test_initialization.py
@@ -10,7 +10,7 @@
import pytest
from jax_dataclasses import replace
from toop_engine_dc_solver.jax.inputs import load_static_information
-from toop_engine_dc_solver.jax.types import BBOutageBaselineAnalysis
+from toop_engine_dc_solver.jax.types import BBOutageBaselineAnalysis, NonRelBBOutageData
from toop_engine_topology_optimizer.dc.genetic_functions.genotype import Genotype
from toop_engine_topology_optimizer.dc.genetic_functions.initialization import (
get_repertoire_metrics,
@@ -142,6 +142,53 @@ def test_update_static_information_removes_busbar_data_when_disabled(static_info
assert updated.dynamic_information.action_set.rel_bb_outage_data is None
+def test_update_static_information_extends_contingency_ids_for_bb_outage_nminus1(static_information_file: str) -> None:
+ static_information = load_static_information(static_information_file)
+ n_timesteps = static_information.dynamic_information.n_timesteps
+ base_cases = (
+ static_information.dynamic_information.n_outages
+ + static_information.dynamic_information.n_multi_outages
+ + static_information.dynamic_information.n_inj_failures
+ )
+
+ static_information = replace(
+ static_information,
+ solver_config=replace(
+ static_information.solver_config,
+ contingency_ids=[f"cont_{i}" for i in range(base_cases)],
+ ),
+ dynamic_information=replace(
+ static_information.dynamic_information,
+ non_rel_bb_outage_data=NonRelBBOutageData(
+ branch_outages=jnp.zeros((2, 1), dtype=int),
+ nodal_indices=jnp.zeros((2,), dtype=int),
+ deltap=jnp.zeros((2, n_timesteps), dtype=float),
+ ),
+ bb_outage_baseline_analysis=BBOutageBaselineAnalysis(
+ overload=jnp.array(1.0),
+ success_count=jnp.array(2),
+ more_splits_penalty=jnp.array(50.0),
+ overload_weight=static_information.dynamic_information.branch_limits.overload_weight,
+ max_mw_flow=static_information.dynamic_information.branch_limits.max_mw_flow,
+ ),
+ ),
+ )
+
+ updated = update_static_information(
+ (static_information,),
+ batch_size=3,
+ enable_nodal_inj_optim=False,
+ enable_parallel_pst_group_optim=False,
+ enable_bb_outage=True,
+ bb_outage_as_nminus1=True,
+ clip_bb_outage_penalty=False,
+ bb_outage_more_islands_penalty=125.0,
+ )[0]
+
+ assert updated.dynamic_information.bb_outage_baseline_analysis is None
+ assert len(updated.solver_config.contingency_ids) == base_cases + updated.dynamic_information.n_bb_outages
+
+
def test_initialize_genetic_algorithm(
static_information_file: str,
) -> None:
diff --git a/packages/topology_optimizer_pkg/tests/interfaces/messages/test_ac_ga_params.py b/packages/topology_optimizer_pkg/tests/interfaces/messages/test_ac_ga_params.py
index b5a3e01c7..77d813978 100644
--- a/packages/topology_optimizer_pkg/tests/interfaces/messages/test_ac_ga_params.py
+++ b/packages/topology_optimizer_pkg/tests/interfaces/messages/test_ac_ga_params.py
@@ -24,6 +24,7 @@ def test_acga_parameters_default():
assert params.reject_critical_branch_threshold == 1.1
assert params.reject_voltage_jump_threshold == 1.1
assert params.reject_critical_va_diff_threshold == 1.1
+ assert params.enable_critical_voltage_rejection is False
assert params.critical_voltage_jump_percent == 5.0
assert params.critical_va_diff_degree == 20.0
# Probabilities sum to one
@@ -54,6 +55,7 @@ def test_acga_parameters_filter_strategy():
reject_critical_branch_threshold=0.8,
reject_voltage_jump_threshold=0.7,
reject_critical_va_diff_threshold=0.85,
+ enable_critical_voltage_rejection=True,
critical_voltage_jump_percent=7.5,
critical_va_diff_degree=12.0,
filter_strategy=filter_strat,
diff --git a/packages/topology_optimizer_pkg/tests/test_ac_dc_integration.py b/packages/topology_optimizer_pkg/tests/test_ac_dc_integration.py
index 5a994c347..5145044dd 100644
--- a/packages/topology_optimizer_pkg/tests/test_ac_dc_integration.py
+++ b/packages/topology_optimizer_pkg/tests/test_ac_dc_integration.py
@@ -41,6 +41,7 @@
from toop_engine_topology_optimizer.ac.scoring_functions import compute_metrics_single_timestep
from toop_engine_topology_optimizer.ac.worker import Args as ACArgs
from toop_engine_topology_optimizer.ac.worker import main as ac_main
+from toop_engine_topology_optimizer.dc.worker.optimizer import extract_topologies, initialize_optimization, run_epoch
from toop_engine_topology_optimizer.dc.worker.worker import Args as DCArgs
from toop_engine_topology_optimizer.dc.worker.worker import main as dc_main
from toop_engine_topology_optimizer.interfaces.messages.ac_params import ACGAParameters, ACOptimizerParameters
@@ -64,6 +65,40 @@
# Ensure that tests using Kafka are not run in parallel with each other
pytestmark = pytest.mark.xdist_group("kafka")
+GROUPED_PST_IDS = ("PST_1_group_1", "PST_2_group_1", "PST_3_group_2", "PST_4_group_2")
+
+
+def _dc_n0_overload_for_pst_setpoints(
+ grid_folder: Path,
+ pst_setpoints: list[int] | None,
+) -> float:
+ network_data = load_grid(
+ data_folder_dirfs=DirFileSystem(str(grid_folder)),
+ parameters=PreprocessParameters(),
+ lf_params=SINGLE_SLACK,
+ )[2]
+ runner = PowsyblRunner()
+ runner.load_base_grid(grid_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"])
+ runner.store_action_set(extract_action_set(network_data))
+ nminus1_definition = extract_nminus1_definition(network_data)
+ runner.store_nminus1_definition(nminus1_definition)
+ base_case_id = nminus1_definition.base_case.id if nminus1_definition.base_case is not None else None
+
+ loadflow = runner.run_loadflow_single_timestep(
+ actions=[],
+ disconnections=[],
+ pst_setpoints=pst_setpoints,
+ method="dc",
+ )
+ metrics = compute_metrics_single_timestep(
+ actions=[],
+ disconnections=[],
+ loadflow=loadflow,
+ additional_info=None,
+ base_case_id=base_case_id,
+ )
+ return float(metrics.extra_scores["overload_energy_n_0"])
+
def dc_main_wrapper(args: DCArgs, processed_gridfile_fs: AbstractFileSystem) -> None:
instance_id = str(uuid4())
@@ -781,6 +816,77 @@ def test_dc_optimizer_fitness_ac_validation_fitness_parallel_pst(tmp_path_factor
)
+@pytest.mark.parametrize("grid_fixture_name", ["grouped_pst_grid_path", "grouped_pst_grid_split_path"])
+def test_grouped_pst_optimizer_improves_n0_loadflow(request: pytest.FixtureRequest, grid_fixture_name: str) -> None:
+ """Run the DC optimizer on grouped PST grids and validate the emitted PST setpoints on DC N-0 loadflow."""
+ grid_folder = request.getfixturevalue(grid_fixture_name)
+ _, static_information, network_data = load_grid(
+ data_folder_dirfs=DirFileSystem(str(grid_folder)),
+ parameters=PreprocessParameters(),
+ lf_params=SINGLE_SLACK,
+ )
+ action_set = extract_action_set(network_data)
+ nodal_injection_information = static_information.dynamic_information.nodal_injection_information
+ assert nodal_injection_information is not None
+ parallel_pst_group_mask = nodal_injection_information.parallel_pst_group_mask
+ assert parallel_pst_group_mask is not None
+ assert [pst.id for pst in action_set.pst_ranges] == list(GROUPED_PST_IDS)
+ assert len(action_set.pst_ranges) == parallel_pst_group_mask.shape[1]
+
+ initial_n0_overload = _dc_n0_overload_for_pst_setpoints(grid_folder, pst_setpoints=None)
+ assert initial_n0_overload > 0.0
+
+ optimizer_params = DCOptimizerParameters(
+ ga_config=BatchedMEParameters(
+ iterations_per_epoch=40,
+ runtime_seconds=5,
+ random_seed=42,
+ enable_nodal_inj_optim=True,
+ enable_parallel_pst_group_optim=True,
+ n_worst_contingencies=2,
+ pst_mutation_sigma=3.0,
+ pst_mutation_probability=1.0,
+ target_metrics=(("overload_energy_n_0", 1.0),),
+ observed_metrics=("overload_energy_n_0", "split_subs"),
+ me_descriptors=(DescriptorDef(metric="split_subs", num_cells=2),),
+ random_topo_prob=0.0,
+ add_split_prob=0.0,
+ change_split_prob=0.0,
+ remove_split_prob=0.0,
+ ),
+ loadflow_solver_config=LoadflowSolverParameters(batch_size=16, max_num_splits=1, max_num_disconnections=0),
+ )
+ optimizer_data, _stats, initial_strategy = initialize_optimization(
+ params=optimizer_params,
+ optimization_id="test_grouped_pst",
+ static_information_files=(PREPROCESSING_PATHS["static_information_file_path"],),
+ processed_gridfile_fs=DirFileSystem(str(grid_folder)),
+ )
+ assert initial_strategy.timesteps[0].metrics.extra_scores["overload_energy_n_0"] > 0.0
+
+ optimizer_data = run_epoch(optimizer_data)
+ topologies = extract_topologies(optimizer_data)
+ topology = max(topologies, key=lambda candidate: candidate.metrics.fitness)
+ assert topology.pst_setpoints is not None
+ pst_setpoints = np.asarray(topology.pst_setpoints, dtype=int)
+ assert pst_setpoints.shape == (parallel_pst_group_mask.shape[1],)
+ for group_mask in np.asarray(parallel_pst_group_mask, dtype=bool):
+ group_setpoints = pst_setpoints[group_mask]
+ assert np.all(group_setpoints == group_setpoints[0])
+ assert (
+ topology.metrics.extra_scores["overload_energy_n_0"]
+ < initial_strategy.timesteps[0].metrics.extra_scores["overload_energy_n_0"]
+ )
+
+ optimized_n0_overload = _dc_n0_overload_for_pst_setpoints(
+ grid_folder,
+ pst_setpoints=topology.pst_setpoints,
+ )
+ assert optimized_n0_overload < initial_n0_overload
+ if grid_fixture_name == "grouped_pst_grid_split_path":
+ assert optimized_n0_overload == pytest.approx(0.0, abs=1e-6)
+
+
def test_dc_optimizer_fitness_ac_validation_fitness_complex(tmp_path_factory: pytest.TempPathFactory) -> None:
"""Compare DC solver fitness to AC validation fitness (using DC loadflow mode) for several PST taps on the complex grid.