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.