From 3bc451013d28f48565baad9c24e9ed3a242ab21a Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Wed, 3 Jun 2026 15:40:05 +0200 Subject: [PATCH 01/24] refactor: change asset topology setup Signed-off-by: Leonard Hilfrich --- .../initial_topology/asset_topology.json | 907 ++++++++++-------- docs/interfaces/asset_topology.md | 22 +- .../toop_engine_dc_solver/example_classes.py | 74 +- .../toop_engine_dc_solver/example_grids.py | 16 +- .../export/disconnection_switch_updates.py | 9 +- .../toop_engine_dc_solver/export/export.py | 8 +- .../export/station_switch_updates.py | 29 +- .../apply_asset_topo_pandapower.py | 22 +- .../postprocess/apply_asset_topo_powsybl.py | 54 +- .../postprocess/postprocess_pandapower.py | 11 +- .../postprocess/postprocess_powsybl.py | 7 +- .../postprocess/realize_assignment.py | 8 +- .../preprocess/action_set.py | 8 +- .../preprocess/network_data.py | 26 +- .../preprocess/preprocess.py | 15 +- .../preprocess/preprocess_bb_outage.py | 22 +- .../preprocess_station_realisations.py | 10 +- .../preprocess/preprocess_switching.py | 102 +- packages/dc_solver_pkg/tests/conftest.py | 8 +- .../tests/deprecated/assignment.py | 12 +- .../export/test_asset_topology_to_dgs.py | 12 +- .../test_disconnection_switch_updates.py | 54 +- .../dc_solver_pkg/tests/export/test_export.py | 51 +- .../export/test_station_switch_updates.py | 73 +- .../initial_topology/asset_topology.json | 907 ++++++++++-------- .../tests/jax/test_busbar_outage.py | 35 +- .../test_apply_asset_topo_pandapower.py | 6 +- .../test_apply_asset_topo_powsybl.py | 62 +- .../tests/preprocessing/test_preprocess.py | 29 +- .../test_preprocess_bb_outage.py | 28 +- .../test_preprocess_switching.py | 24 +- .../preprocessing/test_realize_assignment.py | 14 +- .../preprocessing/test_write_aux_data.py | 4 +- .../dc_solver_pkg/tests/test_example_grids.py | 4 +- .../powsybl/example_grids.py | 8 +- .../powsybl/powsybl_asset_topo.py | 99 +- .../tests/powsybl/test_powsybl_asset_topo.py | 80 +- .../exporter/asset_topology_to_ucte.py | 6 +- .../network_graph/powsybl_station_to_graph.py | 108 ++- .../pandapower_import/asset_topology.py | 36 +- .../pandapower_import/preprocessing.py | 13 +- .../pypowsybl_import/__init__.py | 4 +- .../pypowsybl_import/preprocessing.py | 67 +- .../exporter/test_asset_topology_to_ucte.py | 55 +- .../test_powsybl_station_to_graph.py | 393 ++------ .../test_asset_topology_pandapower.py | 10 +- .../test_preprocessing_pandapower.py | 7 +- .../pypowsybl_import/test_preprocessing.py | 64 ++ packages/interfaces_pkg/README.md | 1 + .../toop_engine_interfaces/asset_topology.py | 645 ++++++++++--- .../asset_topology_helpers.py | 132 +-- .../stored_action_set.py | 22 +- .../tests/test_asset_topology.py | 303 +++++- .../tests/test_asset_topology_helpers.py | 330 +++++-- .../tests/test_stored_action_set.py | 425 +++++--- .../test_stored_action_set_performance.py | 51 +- 56 files changed, 3428 insertions(+), 2104 deletions(-) diff --git a/data/test_grid_node_breaker/initial_topology/asset_topology.json b/data/test_grid_node_breaker/initial_topology/asset_topology.json index 1083d710e..855e3e2a8 100644 --- a/data/test_grid_node_breaker/initial_topology/asset_topology.json +++ b/data/test_grid_node_breaker/initial_topology/asset_topology.json @@ -1,420 +1,487 @@ -{ - "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": "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, + "raw_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": null + }, + { + "grid_model_id": "BBS2_2", + "type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": null + }, + { + "grid_model_id": "BBS2_3", + "type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": null + } + ], + "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 + } + ], + "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": [], + "asset_ids": [ + "L1", + "L2", + "L6", + "L7", + "L8", + "load1" + ], + "asset_branch_ends": [ + null, + null, + null, + null, + null, + null + ], + "asset_bay_ids": [ + "VL2_0::L1::bay", + "VL2_0::L2::bay", + "VL2_0::L6::bay", + "VL2_0::L7::bay", + "VL2_0::L8::bay", + "VL2_0::load1::bay" + ] + }, + { + "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": null + }, + { + "grid_model_id": "BBS3_2", + "type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": null + }, + { + "grid_model_id": "BBS3_3", + "type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": null + }, + { + "grid_model_id": "BBS3_4", + "type": "busbar", + "name": "bus4", + "int_id": 3, + "in_service": true, + "bus_branch_bus_id": null + } + ], + "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 + } + ], + "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_ids": [ + "L3", + "L6", + "L7", + "L9", + "load2" + ], + "asset_branch_ends": [ + null, + null, + null, + null, + null + ], + "asset_bay_ids": [ + "VL3_0::L3::bay", + "VL3_0::L6::bay", + "VL3_0::L7::bay", + "VL3_0::L9::bay", + "VL3_0::load2::bay" + ] + } + ], + "assets": [ + { + "grid_model_id": "L1", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L2", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L6", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L7", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L8", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "load1", + "type": "LOAD", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L3", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L9", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "load2", + "type": "LOAD", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + } + ], + "asset_bays": [ + { + "asset_bay_id": "VL2_0::L1::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" + } + }, + { + "asset_bay_id": "VL2_0::L2::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" + } + }, + { + "asset_bay_id": "VL2_0::L6::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" + } + }, + { + "asset_bay_id": "VL2_0::L7::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" + } + }, + { + "asset_bay_id": "VL2_0::L8::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" + } + }, + { + "asset_bay_id": "VL2_0::load1::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_bay_id": "VL3_0::L3::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" + } + }, + { + "asset_bay_id": "VL3_0::L6::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" + } + }, + { + "asset_bay_id": "VL3_0::L7::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" + } + }, + { + "asset_bay_id": "VL3_0::L9::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" + } + }, + { + "asset_bay_id": "VL3_0::load2::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_setpoints": null, + "timestamp": "2025-05-16T15:25:10.814573", + "metrics": null +} diff --git a/docs/interfaces/asset_topology.md b/docs/interfaces/asset_topology.md index 12ad1337d..07061d990 100644 --- a/docs/interfaces/asset_topology.md +++ b/docs/interfaces/asset_topology.md @@ -10,11 +10,16 @@ Asset Topology is essential when stations do not allow free assignment of lines Collection of time steps, each represented by a [`Topology`][toop_engine_interfaces.asset_topology.Topology]. - [`Topology`][toop_engine_interfaces.asset_topology.Topology] - Contains multiple [`Station`][toop_engine_interfaces.asset_topology.Station] objects and optional [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] objects. + Stores lean [`TopologyStation`][toop_engine_interfaces.asset_topology.TopologyStation] records in `raw_stations`, topology-owned canonical assets in `assets`, and optional [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] objects. Rich [`Station`][toop_engine_interfaces.asset_topology.Station] objects are reconstructed with [`Topology.materialize_stations()`][toop_engine_interfaces.asset_topology.Topology.materialize_stations]. - [`Station`][toop_engine_interfaces.asset_topology.Station] Contains lists of [`Busbar`][toop_engine_interfaces.asset_topology.Busbar], [`BusbarCoupler`][toop_engine_interfaces.asset_topology.BusbarCoupler], and [`SwitchableAsset`][toop_engine_interfaces.asset_topology.SwitchableAsset]. - Includes asset_switching_table, the current switch connection layout and asset_connectivity, all possible selections. + Includes `asset_switching_table`, the current switch connection layout and `asset_connectivity`, all possible selections. + The `grid_model_id` refers to the bus-branch bus id of the splitable station view, not to the full voltage level id. + +- [`TopologyStation`][toop_engine_interfaces.asset_topology.TopologyStation] + Stores the lean station representation used inside [`Topology`][toop_engine_interfaces.asset_topology.Topology]. + Instead of embedded asset payloads it stores aligned station-local arrays `asset_ids`, `asset_branch_ends`, and `asset_bay_ids`. - [`Busbar`][toop_engine_interfaces.asset_topology.Busbar] Represents a single busbar in a station. @@ -63,6 +68,19 @@ See the [`Asset Topology Reference`][toop_engine_interfaces.asset_topology] for ## How to use / Implementation +## Station Identity And Asset Scope + +The intended station contract is bus-view based: + +- `Station.grid_model_id` and `TopologyStation.grid_model_id` identify the bus-branch bus id of the splitable station view. +- The station busbars belong to that bus view and therefore must carry matching `bus_branch_bus_id` values. +- The station-local asset arrays and switching tables describe which assets are visible in that station view and how they attach locally. + +The current importer implementations are not fully uniform yet: + +- The bus-breaker powsybl helper narrows busbars and assets to the selected `bus_id` before building the station view. +- The node-breaker powsybl importer currently assigns a bus-specific `Station.grid_model_id` but still derives the asset list from the full substation graph. This is broader than the intended "single bus-branch bus" scope and should be treated as current implementation behavior, not as the desired long-term contract. + To populate Asset Topology data from grid models, use the [`Network Graph module`][toop_engine_importer.network_graph]. A Pandapower to Asset Topology implementation is found in the importer: [`get_list_of_stations_ids`][toop_engine_importer.pandapower_import.asset_topology.get_list_of_stations_ids] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py index 10247444c..d5b43184c 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py @@ -14,7 +14,7 @@ AssetBay, Busbar, BusbarCoupler, - Station, + RawStation, SwitchableAsset, Topology, ) @@ -33,8 +33,8 @@ def get_basic_node_breaker_topology() -> Topology: topology_id="test", grid_model_file="test_file", name=None, - stations=[ - Station( + raw_stations=[ + RawStation( grid_model_id="VL4_0", name="VLevel4", type=None, @@ -55,49 +55,39 @@ def get_basic_node_breaker_topology() -> Topology: in_service=True, ) ], - assets=[ - SwitchableAsset( - grid_model_id="L4", - type="LINE", - name="", - in_service=True, - branch_end=None, - asset_bay=AssetBay( - sl_switch_grid_model_id=None, - dv_switch_grid_model_id="L42_BREAKER", - sr_switch_grid_model_id={"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, - ), - ), - SwitchableAsset( - grid_model_id="L5", - type="LINE", - name="", - in_service=True, - branch_end=None, - asset_bay=AssetBay( - sl_switch_grid_model_id=None, - dv_switch_grid_model_id="L52_BREAKER", - sr_switch_grid_model_id={"BBS4_1": "L52_DISCONNECTOR_5_0", "BBS4_2": "L52_DISCONNECTOR_5_1"}, - ), - ), - SwitchableAsset( - grid_model_id="L8", - type="LINE", - name="", - in_service=True, - branch_end=None, - asset_bay=AssetBay( - sl_switch_grid_model_id=None, - 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_ids=["L4", "L5", "L8"], + asset_branch_ends=[None, None, None], + asset_bay_ids=["VL4_0::L4::bay", "VL4_0::L5::bay", "VL4_0::L8::bay"], asset_switching_table=np.array([[False, False, False], [True, True, False]]), asset_connectivity=np.array([[True, True, True], [True, True, True]]), ) ], - asset_setpoints=None, + assets=[ + SwitchableAsset(grid_model_id="L4", type="LINE", name="", in_service=True, branch_end=None), + SwitchableAsset(grid_model_id="L5", type="LINE", name="", in_service=True, branch_end=None), + SwitchableAsset(grid_model_id="L8", type="LINE", name="", in_service=True, branch_end=None), + ], + asset_bays=[ + AssetBay( + asset_bay_id="VL4_0::L4::bay", + sl_switch_grid_model_id=None, + dv_switch_grid_model_id="L42_BREAKER", + sr_switch_grid_model_id={"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, + ), + AssetBay( + asset_bay_id="VL4_0::L5::bay", + sl_switch_grid_model_id=None, + dv_switch_grid_model_id="L52_BREAKER", + sr_switch_grid_model_id={"BBS4_1": "L52_DISCONNECTOR_5_0", "BBS4_2": "L52_DISCONNECTOR_5_1"}, + ), + AssetBay( + asset_bay_id="VL4_0::L8::bay", + sl_switch_grid_model_id=None, + dv_switch_grid_model_id="L82_BREAKER", + sr_switch_grid_model_id={"BBS4_1": "L82_DISCONNECTOR_7_0", "BBS4_2": "L82_DISCONNECTOR_7_1"}, + ), + ], timestamp=datetime.datetime(2025, 2, 4, 9, 12, 0, 109256), + asset_setpoints=None, metrics=None, ) 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 f62538a03..4e75d3da6 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 @@ -54,9 +54,10 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, - Station, + MaterializedStation, SwitchableAsset, Topology, + topology_from_materialized_stations, ) from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.folder_structure import ( @@ -158,7 +159,7 @@ class PandapowerCounters: def random_station_info_backend( backend: BackendInterface, node_idx: Integral, pp_counters: Optional[PandapowerCounters] -) -> tuple[Station, Optional[PandapowerCounters]]: +) -> tuple[MaterializedStation, Optional[PandapowerCounters]]: """Generate a random station for any backend This will create a Station object with 2 busbars, 1 coupler and a random assignment of assets @@ -256,7 +257,7 @@ def random_station_info_backend( bus_b_id = global_id + "_b" switch_id = global_id + "_coupler" - return Station( + return MaterializedStation( grid_model_id=global_id, busbars=[ Busbar( @@ -307,10 +308,13 @@ def random_topology_info_backend(backend: BackendInterface, pp_counters: Optiona new_station, pp_counters = random_station_info_backend(backend, node_idx, pp_counters) stations.append(new_station) - return Topology( + return topology_from_materialized_stations( + reference_topology=Topology( + topology_id="random_topology", + raw_stations=[], + timestamp=datetime.datetime.now(), + ), stations=stations, - topology_id="random_topology", - timestamp=datetime.datetime.now(), ) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py index f85820377..0d723e24f 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py @@ -12,7 +12,7 @@ import pandera.typing as pat import structlog from beartype.typing import cast -from toop_engine_interfaces.asset_topology import AssetBay, Station, Topology +from toop_engine_interfaces.asset_topology import AssetBay, MaterializedStation, Topology from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema @@ -20,7 +20,7 @@ def get_disconnected_asset_ids( - stations: list[Station], + stations: list[MaterializedStation], disconnections: list[GridElement], ) -> dict[str, list[AssetBay]]: """Collect representable disconnection asset ids from the provided topology. @@ -73,8 +73,9 @@ def get_changing_switches_from_disconnections( pat.DataFrame[SwitchUpdateSchema] Switch update rows representing the requested disconnections where possible. """ + stations = starting_topology.materialize_stations() disconnection_asset_map: dict[str, list[AssetBay]] = get_disconnected_asset_ids( - stations=starting_topology.stations, + stations=stations, disconnections=disconnections, ) @@ -91,7 +92,7 @@ def get_changing_switches_from_disconnections( disconnection_id=disconnection.id, disconnection_name=disconnection.name, disconnection_type=disconnection.type, - available_station_ids=[station.grid_model_id for station in starting_topology.stations], + available_station_ids=[station.grid_model_id for station in stations], ) for asset in assets: switch_updates.append( diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py index c29cb7440..f8972ab8b 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py @@ -21,7 +21,7 @@ from toop_engine_dc_solver.export.station_switch_updates import ( get_changing_switches_from_changed_stations, ) -from toop_engine_interfaces.asset_topology import Station, Topology +from toop_engine_interfaces.asset_topology import MaterializedStation, Topology from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.stored_action_set import ActionSet @@ -33,7 +33,7 @@ def _get_changed_stations_from_action_indices( action_set: ActionSet, actions: list[int], -) -> list[Station]: +) -> list[MaterializedStation]: """Resolve action indices to concrete changed stations. Parameters @@ -53,7 +53,7 @@ def _get_changed_stations_from_action_indices( ValueError If any action index is negative or beyond the available range. """ - changed_stations: list[Station] = [] + changed_stations: list[MaterializedStation] = [] for action_index in actions: if action_index < 0 or action_index >= len(action_set.local_actions): raise ValueError(f"Action index {action_index} is out of bounds for the action set") @@ -94,7 +94,7 @@ def _get_disconnections_from_indices( @pa.check_types def get_changing_switches_from_actions( - changed_stations: list[Station], + changed_stations: list[MaterializedStation], simplified_starting_topology: Topology, disconnections: list[GridElement] | None = None, full_starting_topology: Topology | None = None, diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py index 844c2eae9..269031aff 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py @@ -17,20 +17,20 @@ import pandera.typing as pat from beartype.typing import cast from jaxtyping import Bool -from toop_engine_interfaces.asset_topology import Station, Topology +from toop_engine_interfaces.asset_topology import MaterializedStation, Topology from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema -def _get_busbar_lookup(station: Station) -> dict[int, str]: +def _get_busbar_lookup(station: MaterializedStation) -> dict[int, str]: """Map busbar row indices in the switching table to busbar ids.""" return {index: busbar.grid_model_id for index, busbar in enumerate(station.busbars)} def _resolve_changed_stations( - changed_stations: list[Station], + changed_stations: list[MaterializedStation], starting_topology: Topology, -) -> tuple[dict[str, Station], dict[str, Station], list[str]]: +) -> tuple[dict[str, MaterializedStation], dict[str, MaterializedStation], list[str]]: """Resolve station lookups and preserve changed-station ordering. This helper is intentionally limited to station actions. It validates that all changed stations @@ -62,21 +62,22 @@ def _resolve_changed_stations( if len(changed_station_ids) != len(set(changed_station_ids)): raise ValueError("Changed stations must be unique by grid_model_id.") - starting_station_lookup = {station.grid_model_id: station for station in starting_topology.stations} + starting_stations = starting_topology.materialize_stations() + starting_station_lookup = {station.grid_model_id: station for station in starting_stations} changed_station_lookup = {station.grid_model_id: station for station in changed_stations} missing_station_ids = set(changed_station_lookup).difference(starting_station_lookup) if missing_station_ids: raise ValueError(f"Changed stations not found in starting topology: {sorted(missing_station_ids)}") ordered_changed_station_ids = [ - station.grid_model_id for station in starting_topology.stations if station.grid_model_id in changed_station_lookup + station.grid_model_id for station in starting_stations if station.grid_model_id in changed_station_lookup ] return starting_station_lookup, changed_station_lookup, ordered_changed_station_ids def _get_coupler_switch_diffs( - changed_station: Station, - starting_station: Station, + changed_station: MaterializedStation, + starting_station: MaterializedStation, ) -> list[dict[str, str | bool]]: """Collect coupler switch changes between two station states. @@ -121,8 +122,8 @@ def _get_coupler_switch_diffs( def _get_asset_switch_diffs( - changed_station: Station, - starting_station: Station, + changed_station: MaterializedStation, + starting_station: MaterializedStation, fail_on_disconnect: bool = False, ) -> list[dict[str, str | bool]]: """Collect selector and breaker switch changes between two station states. @@ -201,8 +202,8 @@ def _get_asset_switch_diffs( def _get_switch_updates_from_station_ids( - changed_station_lookup: dict[str, Station], - starting_station_lookup: dict[str, Station], + changed_station_lookup: dict[str, MaterializedStation], + starting_station_lookup: dict[str, MaterializedStation], ordered_station_ids: list[str], ) -> pat.DataFrame[SwitchUpdateSchema]: """Build switch updates for a specific ordered list of stations. @@ -242,7 +243,7 @@ def _get_switch_updates_from_station_ids( return cast(pat.DataFrame[SwitchUpdateSchema], diff_switch_df) -def _get_station_switching_table(station: Station) -> Bool[np.ndarray, " n_busbar n_asset"]: +def _get_station_switching_table(station: MaterializedStation) -> Bool[np.ndarray, " n_busbar n_asset"]: """Return the station switching table as a boolean numpy array. Parameters @@ -260,7 +261,7 @@ def _get_station_switching_table(station: Station) -> Bool[np.ndarray, " n_busba @pa.check_types def get_changing_switches_from_changed_stations( - changed_stations: list[Station], + changed_stations: list[MaterializedStation], starting_topology: Topology, ) -> pat.DataFrame[SwitchUpdateSchema]: """Get changed switches by comparing changed stations to the starting topology. diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py index d48240a2a..889ab323c 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py @@ -21,11 +21,11 @@ from toop_engine_grid_helpers.pandapower.pandapower_helpers import get_element_table, get_remotely_connected_buses from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import parse_globally_unique_id, table_id from toop_engine_interfaces.asset_topology import ( + AppliedStation, Busbar, BusbarCoupler, - RealizedStation, + MaterializedStation, RealizedTopology, - Station, Topology, ) from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs, find_busbars_for_coupler @@ -278,7 +278,7 @@ def delete_excess_switches( def apply_station_assets( net: pp.pandapowerNet, - station: Station, + station: MaterializedStation, ) -> tuple[list[int], list[tuple[int, int, bool]]]: """Apply a station topology to a pandapower network. @@ -286,13 +286,13 @@ def apply_station_assets( If the assets are connected via asset bays and switches before the application of this function, these asset bays will be dangling after. - It returns diffs compatible with the RealizedStation format. + It returns diffs compatible with the AppliedStation format. Parameters ---------- net : pandapowerNet The pandapower network to apply the topology to. Will be modified in place. - station : Station + station : MaterializedStation The station to apply. It is assumed that the station grid_model_id refers to a bus in the pandapower network. Returns @@ -423,8 +423,8 @@ class ApplyGridDiff: def apply_station( net: pp.pandapowerNet, - station: Station, -) -> tuple[ApplyGridDiff, RealizedStation]: + station: MaterializedStation, +) -> tuple[ApplyGridDiff, AppliedStation]: """Apply an asset topology station to a pandapower network. This will force the station into the format of the asset topology, meaning missing busbars and switches will be created, @@ -441,7 +441,7 @@ def apply_station( ---------- net : pandapowerNet The pandapower network to apply the topology to. Will be modified in place. - station : Station + station : MaterializedStation The station to apply. It is assumed that the station grid_model_id refers to a bus in the pandapower network. Returns @@ -449,7 +449,7 @@ def apply_station( ApplyGridDiff The difference between the switches and busbars that were expected by the asset topology and the actual switches and busbars in the grid. - RealizedStation + AppliedStation The realized station, containing the coupler diff, reassignment diff and disconnection diff. """ busbars_created = create_missing_busbars( @@ -506,7 +506,7 @@ def apply_station( busbars_deleted=busbars_deleted, switches_deleted=switches_deleted, ), - RealizedStation( + AppliedStation( station=station, coupler_diff=coupler_diff, reassignment_diff=reassignment_diff, @@ -538,7 +538,7 @@ def apply_topology(net: pp.pandapowerNet, topology: Topology) -> tuple[list[tupl RealizedTopology The realized topology, containing the coupler diff, reassignment diff and disconnection diff for each station. """ - realizations = [apply_station(net, station) for station in topology.stations] + realizations = [apply_station(net, station) for station in topology.materialize_stations()] apply_diffs = [(rs.station.grid_model_id, apply_diff) for apply_diff, rs in realizations] realized_stations = [rs for _, rs in realizations] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index 711e1dc2b..de2ac6125 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -18,8 +18,6 @@ the node/breaker way is still TODO. """ -from datetime import datetime - import numpy as np import pandas as pd import pandera as pa @@ -29,11 +27,13 @@ from pypowsybl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import assert_station_in_network from toop_engine_interfaces.asset_topology import ( + AppliedStation, BusbarCoupler, - RealizedStation, + MaterializedStation, + RawStation, RealizedTopology, - Station, Topology, + copy_topology_with_updates, ) from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model @@ -57,7 +57,7 @@ def get_coupler_states_from_busbar_couplers(station_couplers: list[BusbarCoupler return cast(pat.DataFrame[SwitchUpdateSchema], switch_df) -def get_asset_bay_grid_model_id_list(station: Station) -> list[dict[str, str] | None]: +def get_asset_bay_grid_model_id_list(station: MaterializedStation) -> list[dict[str, str] | None]: """Get selector switch ids for each station asset.""" asset_bays = [asset.asset_bay for asset in station.assets] sr_switch_grid_model_id_list: list[dict[str, str] | None] = [] @@ -69,14 +69,14 @@ def get_asset_bay_grid_model_id_list(station: Station) -> list[dict[str, str] | return sr_switch_grid_model_id_list -def get_busbar_lookup(station: Station) -> dict[int, str]: +def get_busbar_lookup(station: MaterializedStation) -> dict[int, str]: """Get the busbar lookup for the given station.""" return {index: busbar.grid_model_id for index, busbar in enumerate(station.busbars)} @pa.check_types def get_asset_switch_states_from_station( - station: Station, + station: MaterializedStation, ) -> tuple[pat.DataFrame[SwitchUpdateSchema], pat.DataFrame[SwitchUpdateSchema]]: """Translate asset selector and breaker states of one station to switch updates.""" switch_reassignment_list: list[dict[str, str | bool]] = [] @@ -157,7 +157,7 @@ def get_diff_switch_states( def get_changing_switches_from_topology(network: Network, target_topology: Topology) -> pat.DataFrame[SwitchUpdateSchema]: """Get the switch updates needed to realize a target topology on a node-breaker network.""" switch_update_df = get_empty_dataframe_from_model(SwitchUpdateSchema) - for station in target_topology.stations: + for station in target_topology.materialize_stations(): coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) station_switch_updates = pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True) @@ -331,7 +331,7 @@ def disconnect_injection( def apply_single_asset_bus_branch( net: Network, - station: Station, + station: MaterializedStation, asset_index: int, ) -> tuple[Literal["disconnected", "reassigned", "nothing"], list[tuple[int, int, bool]]]: """Reassign or disconnect a single asset in a bus/branch topology @@ -455,7 +455,7 @@ def set_coupler( return True -def apply_station_bus_branch(net: Network, station: Station) -> RealizedStation: +def apply_station_bus_branch(net: Network, station: MaterializedStation) -> AppliedStation: """Apply a station topology to a powsybl model in bus/branch format This will assume that the substations are in bus/branch format and that the busbars in the station are the same as in @@ -469,13 +469,13 @@ def apply_station_bus_branch(net: Network, station: Station) -> RealizedStation: busbar. Furthermore, we assume to find the bus-breaker buses in the voltage level to represent the busbars in the asset topology. If there are more buses, these additional buses will be ignored, if there are fewer buses, an exception is raised. Will be modified in-place. - station : Station + station : MaterializedStation The asset topology station. The switching state of the assets and busbar couplers shall be applied to the matched station in the powsybl grid Returns ------- - RealizedStation + AppliedStation The realized station object which contains the input station plus a diff of switched couplers, reassignments and disconnections. @@ -501,7 +501,7 @@ def apply_station_bus_branch(net: Network, station: Station) -> RealizedStation: if set_coupler(net, coupler.grid_model_id, coupler.open): coupler_diff.append(coupler) - return RealizedStation( + return AppliedStation( station=station, disconnection_diff=disconnection_diff, reassignment_diff=reassignment_diff, @@ -534,7 +534,7 @@ def apply_topology_bus_branch(net: Network, topology: Topology) -> RealizedTopol The realized topology object which contains the input topology plus a diff of switched couplers, reassignments and disconnections. """ - realized_stations = [apply_station_bus_branch(net, station) for station in topology.stations] + realized_stations = [apply_station_bus_branch(net, station) for station in topology.materialize_stations()] coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) @@ -600,7 +600,9 @@ def is_node_breaker_grid(net: Network, relevant_station: Optional[str] = None) - ) -def apply_station(net: Network, station: Station) -> Union[pa.typing.DataFrame[SwitchUpdateSchema], RealizedStation]: +def apply_station( + net: Network, topology: Topology, raw_station: RawStation +) -> Union[pa.typing.DataFrame[SwitchUpdateSchema], AppliedStation]: """Apply a station topology to a powsybl model This will apply the station topology to the network. If the network is in bus/branch format, it will return the @@ -614,23 +616,23 @@ def apply_station(net: Network, station: Station) -> Union[pa.typing.DataFrame[S busbar. Furthermore, we assume to find the bus-breaker buses in the voltage level to represent the busbars in the asset topology. If there are more buses, these additional buses will be ignored, if there are fewer buses, an exception is raised. Will be modified in-place. - station : Station - The asset topology station. The switching state of the assets and busbar couplers shall be applied to the matched - station in the powsybl grid + topology : Topology + The owning topology that provides canonical assets and asset bays for the raw station. + raw_station : RawStation + The lean station view to apply. The switching state of the assets and busbar couplers shall be applied to the + matched station in the powsybl grid. Returns ------- - Union[pa.typing.DataFrame[SwitchUpdateSchema], RealizedStation] + Union[pa.typing.DataFrame[SwitchUpdateSchema], AppliedStation] The realized station object which contains the input station plus a diff of switched couplers, reassignments and disconnections or a dataframe of switches that were updated. """ - if is_node_breaker_grid(net=net, relevant_station=station.grid_model_id): + station_topology = copy_topology_with_updates(topology, [raw_station], topology.assets, topology.asset_bays) + + if is_node_breaker_grid(net=net, relevant_station=raw_station.grid_model_id): return apply_node_breaker_topology( net=net, - target_topology=Topology( - topology_id="this_id_will_be_ignored", - stations=[station], - timestamp=datetime.now(), - ), + target_topology=station_topology, ) - return apply_station_bus_branch(net=net, station=station) + return apply_station_bus_branch(net=net, station=station_topology.materialize_stations()[0]) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py index 078323814..a5924afab 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py @@ -32,7 +32,7 @@ from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import ( parse_globally_unique_id, ) -from toop_engine_interfaces.asset_topology import RealizedStation, RealizedTopology +from toop_engine_interfaces.asset_topology import AppliedStation, RealizedTopology, topology_from_materialized_stations from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs, electrical_components from toop_engine_interfaces.loadflow_result_helpers_polars import extract_solver_matrices_polars from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars @@ -76,7 +76,7 @@ def apply_topology( """ net = deepcopy(net) - realized_stations: list[RealizedStation] = [] + realized_stations: list[AppliedStation] = [] for action in actions: # Apply the action to the network if action >= len(action_set.local_actions): @@ -86,10 +86,9 @@ def apply_topology( coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) realized_topology = RealizedTopology( - topology=action_set.starting_topology.model_copy( - update={ - "stations": [s.station for s in realized_stations], - } + topology=topology_from_materialized_stations( + action_set.starting_topology, + [s.station for s in realized_stations], ), coupler_diff=coupler_diff, reassignment_diff=reassignment_diff, diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index 49a665995..294e0a7bd 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -46,6 +46,7 @@ extract_single_injection_loadflow_result, load_powsybl_from_fs, ) +from toop_engine_interfaces.asset_topology import topology_from_materialized_stations from toop_engine_interfaces.asset_topology_helpers import electrical_components from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Contingency, Nminus1Definition @@ -135,7 +136,7 @@ def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> A return None stations = [action_set.local_actions[action] for action in actions] - changed_stations_topo = action_set.starting_topology.model_copy(update={"stations": stations}) + changed_stations_topo = topology_from_materialized_stations(action_set.starting_topology, stations) if is_node_breaker_grid(net, stations[0].grid_model_id): additional_info = apply_node_breaker_topology(net, changed_stations_topo) @@ -375,8 +376,8 @@ def _get_relevant_station_id(self) -> Optional[str]: return None for topology in (self.action_set.starting_topology, self.action_set.simplified_starting_topology): - if topology is not None and len(topology.stations): - return topology.stations[0].grid_model_id + if topology is not None and len(topology.raw_stations): + return topology.raw_stations[0].grid_model_id return None @contextmanager diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py index ce7771077..064b8def2 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py @@ -17,7 +17,7 @@ from jaxtyping import Array, Bool, Int from toop_engine_dc_solver.preprocess.helpers.switching_distance import per_station_switching_distance from toop_engine_dc_solver.preprocess.preprocess_switching import OptimalSeparationSetInfo -from toop_engine_interfaces.asset_topology import Station +from toop_engine_interfaces.asset_topology import MaterializedStation from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits logger = structlog.get_logger(__name__) @@ -345,13 +345,13 @@ def compute_switching_table( def realise_ba_to_physical_topo_per_station_jax( local_branch_action_set: Bool[np.ndarray, " n_combinations n_branches"], - station: Station, + station: MaterializedStation, separation_set_info: OptimalSeparationSetInfo, batch_size: int = 1024, choice_heuristic: Literal["first", "least_connected_busbar", "most_connected_busbar"] = "least_connected_busbar", validate: bool = True, reassignment_limits: Optional[ReassignmentLimits] = None, -) -> tuple[list[Station], Bool[np.ndarray, "n_combinations n_branches"], list[list[int]], list[int]]: +) -> tuple[list[MaterializedStation], Bool[np.ndarray, "n_combinations n_branches"], list[list[int]], list[int]]: """Realize the branch actions to physical topology per station. This iterates over all actions in the local branch action set and tries to find a realization for them. @@ -494,7 +494,7 @@ def realise_ba_to_physical_topo_per_station_jax( ] if validate: for realised_station in realised_stations: - Station.model_validate(realised_station) + MaterializedStation.model_validate(realised_station) # Convert the busbar mapping to a list of busbar A mappings busbar_mappings_converted = [np.flatnonzero(~mapping).tolist() for mapping in chosen_busbar_mapping] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py index 6d478d970..c3e8caadd 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py @@ -39,7 +39,7 @@ ) from toop_engine_dc_solver.preprocess.helpers.switching_distance import min_hamming_distance_matrix from toop_engine_dc_solver.preprocess.network_data import NetworkData, get_relevant_stations -from toop_engine_interfaces.asset_topology import Station +from toop_engine_interfaces.asset_topology import RawStation from toop_engine_interfaces.asset_topology_helpers import get_connected_assets from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits @@ -632,7 +632,7 @@ def unpad_branch_actions( def determine_injection_topology_sub( network_data: NetworkData, local_injection_idxs: Int[np.ndarray, " n_injections_at_node"], - station: Station, + station: RawStation, n_local_branch_actions: int, local_busbar_a_mapping: list[list[int]], n_injections_at_node: int, @@ -651,7 +651,7 @@ def determine_injection_topology_sub( The network data containing injection and branch information. local_injection_idxs : list[int] List of local injection indices corresponding to the station. - station : Station + station : RawStation The station object containing information about busbars and connected assets. n_local_branch_actions : int Number of local branch actions to consider. @@ -675,7 +675,7 @@ def determine_injection_topology_sub( bba_connected_injection_ids = [ asset.grid_model_id for bb_index in busbar_a_mapping - for asset in get_connected_assets(station, bb_index) + for asset in get_connected_assets(station, bb_index, network_data.simplified_asset_topology.assets) if not asset.is_branch() ] bba_connected_injection_idxs = [ 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 434509775..4f2c3f259 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 @@ -17,7 +17,7 @@ from jaxtyping import Bool, Float, Int from toop_engine_dc_solver.preprocess.preprocess_switching import OptimalSeparationSetInfo from toop_engine_grid_helpers.powsybl.powsybl_helpers import load_lf_params_from_fs -from toop_engine_interfaces.asset_topology import Station, Topology +from toop_engine_interfaces.asset_topology import MaterializedStation, RawStation, Topology from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.nminus1_definition import Contingency, GridElement, Nminus1Definition from toop_engine_interfaces.stored_action_set import ActionSet, PSTRange @@ -310,7 +310,7 @@ class NetworkData: """The mask over nodes that are a controllable phase shifter. When adding the PSDF matrix, bogus nodes will be included. The ones that refer to a controllable PST will be mentioned in this mask.""" - realised_stations: Optional[list[list[Station]]] = None + realised_stations: Optional[list[list[MaterializedStation]]] = None """The realised stations for each relevant node depending on the branch_actions. The outer list is of length equal to the number of relevant nodes. The inner list if of length equal to the number of branch actions feasible for the given node. Each station is a simplified station.""" @@ -571,34 +571,34 @@ def validate_network_data(network_data: NetworkData) -> None: ): assert len(branch_act) == len(inj_act) == len(sw_dist) - assert len(network_data.simplified_asset_topology.stations) == n_rel_subs + assert len(network_data.simplified_asset_topology.raw_stations) == n_rel_subs assert len(network_data.realised_stations) == n_rel_subs for realizations in network_data.realised_stations: for realized_station in realizations: - Station.model_validate(realized_station) + MaterializedStation.model_validate(realized_station) assert len(network_data.busbar_a_mappings) == n_rel_subs assert len(network_data.branch_action_set_switching_distance) == n_rel_subs -def get_relevant_stations(network_data: NetworkData) -> list[Station]: +def get_relevant_stations(network_data: NetworkData) -> list[RawStation]: """ - Get the relevant asset-topology stations from the network data. + Get the relevant raw asset-topology stations from the network data. Parameters ---------- network_data : NetworkData - The network data containing asset topology. + The network data containing the simplified asset topology and relevant node mask. Returns ------- - list[Station] - A list of relevant stations. + list[RawStation] + The relevant raw stations in the same order as the relevant nodes. """ relevant_node_ids = [ node for node, mask in zip(network_data.node_ids, network_data.relevant_node_mask, strict=True) if mask ] - def find_station(stations: list[Station], grid_model_id: str, fallback: Optional[Station] = None) -> Station: + def find_station(stations: list[RawStation], grid_model_id: str, fallback: Optional[RawStation] = None) -> RawStation: for station in stations: if station.grid_model_id == grid_model_id: return station @@ -606,7 +606,7 @@ def find_station(stations: list[Station], grid_model_id: str, fallback: Optional return fallback raise ValueError(f"Could not find station with grid_model_id {grid_model_id}") - return [find_station(network_data.simplified_asset_topology.stations, node_id) for node_id in relevant_node_ids] + return [find_station(network_data.simplified_asset_topology.raw_stations, node_id) for node_id in relevant_node_ids] def map_branch_injection_ids( @@ -739,13 +739,13 @@ def extract_nminus1_definition(network_data: NetworkData) -> Nminus1Definition: ) monitored_nodes = [ GridElement(id=busbar.grid_model_id, name=busbar.name or "", type=busbar.type, kind="bus") - for station in asset_topology.stations + for station in asset_topology.materialize_stations() for busbar in station.busbars ] monitored_switches = [ GridElement(id=switch.grid_model_id, name=switch.name or "", type=switch.type, kind="switch") - for station in asset_topology.stations + for station in asset_topology.materialize_stations() for switch in station.couplers ] 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 9a46802a1..2e6335798 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 @@ -72,6 +72,7 @@ make_optimal_separation_set, prepare_for_separation_set, ) +from toop_engine_interfaces.asset_topology import topology_from_materialized_stations from toop_engine_interfaces.asset_topology_helpers import order_topology from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.messages.preprocess.preprocess_commands import PreprocessParameters, ReassignmentLimits @@ -195,7 +196,7 @@ def filter_relevant_nodes_no_asset_station(network_data: NetworkData) -> Network """ assert network_data.asset_topology is not None, "Asset topology has to be passed in" relevant_node_ids = np.array(network_data.node_ids)[np.flatnonzero(network_data.relevant_node_mask)] - station_ids = np.array([station.grid_model_id for station in network_data.asset_topology.stations]) + station_ids = np.array([station.grid_model_id for station in network_data.asset_topology.materialize_stations()]) keep_mask = np.isin(relevant_node_ids, station_ids) for node_id in relevant_node_ids[~keep_mask]: @@ -947,9 +948,9 @@ def remove_relevant_subs( simplified_asset_topology = ( network_data.simplified_asset_topology.model_copy( update={ - "stations": [ + "raw_stations": [ x - for x, has_action in zip(network_data.simplified_asset_topology.stations, keep_mask, strict=True) + for x, has_action in zip(network_data.simplified_asset_topology.raw_stations, keep_mask, strict=True) if has_action ] } @@ -1195,7 +1196,7 @@ def simplify_asset_topology(network_data: NetworkData, close_couplers: bool = Fa network_data.relevant_nodes, network_data.branches_at_nodes, network_data.injection_idx_at_nodes, - topology.stations, + topology.materialize_stations(), strict=True, ): assert network_data.node_ids[node_index] == station.grid_model_id, "The station id does not match the node id" @@ -1223,9 +1224,7 @@ def simplify_asset_topology(network_data: NetworkData, close_couplers: bool = Fa network_data = replace( network_data, - simplified_asset_topology=topology.model_copy( - update={"stations": stations}, - ), + simplified_asset_topology=topology_from_materialized_stations(topology, stations), ) return remove_relevant_subs(network_data, np.array(keep_mask, dtype=bool)) @@ -1254,7 +1253,7 @@ def compute_separation_set_for_stations( assert network_data.simplified_asset_topology is not None, "Please simplify the asset topology first" actions = 0 separation_sets_info: list[OptimalSeparationSetInfo] = [] - for station in network_data.simplified_asset_topology.stations: + for station in network_data.simplified_asset_topology.materialize_stations(): separation_set_info = make_optimal_separation_set(station, clip_hamming_distance, clip_at_size) separation_sets_info.append(separation_set_info) actions += separation_set_info.separation_set.shape[0] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py index c1022f43a..02dafbb34 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py @@ -19,7 +19,7 @@ OutageData, get_relevant_stations, ) -from toop_engine_interfaces.asset_topology import Station, SwitchableAsset +from toop_engine_interfaces.asset_topology import MaterializedStation, RawStation, SwitchableAsset from toop_engine_interfaces.asset_topology_helpers import find_station_by_id, get_connected_assets logger = structlog.get_logger(__name__) @@ -86,7 +86,7 @@ def get_total_injection_along_stub_branch( return total_injection -def get_busbar_index(station: Station, busbar_id: str) -> int: +def get_busbar_index(station: MaterializedStation, busbar_id: str) -> int: """Get the index of a busbar within a station by its grid_model_id of busbar (busbar_id). Parameters @@ -205,7 +205,7 @@ def get_busbar_map_adjacent_branches(network_data: NetworkData) -> Bool[np.ndarr # Gather all branches connected to a station with a busbar outage for station_id in network_data.busbar_outage_map.keys(): # Find the asset topo station id - station = find_station_by_id(network_data.asset_topology.stations, station_id) + station = find_station_by_id(network_data.asset_topology.materialize_stations(), station_id) for asset in station.assets: bb_outage_asset_indices.add(asset.grid_model_id) @@ -213,7 +213,7 @@ def get_busbar_map_adjacent_branches(network_data: NetworkData) -> Bool[np.ndarr return busbar_outage_branch_mask -def get_busbar_branches_map(station: Station, network_data: NetworkData) -> dict[str, list[int]]: +def get_busbar_branches_map(station: MaterializedStation, network_data: NetworkData) -> dict[str, list[int]]: """Map each busbar in a station to the list of branch indices connected to it. These branch_indices index into network_data.branch_ids. @@ -290,7 +290,7 @@ def get_phy_bb_nodal_index( def extract_busbar_outage_data( - station: Station, + station: MaterializedStation, busbar_id: str, network: NetworkData, stub_power_map: dict[str, Float[np.ndarray, " n_timestep"]], @@ -402,7 +402,7 @@ def update_network_data_with_non_rel_bb_outages( delta_p = [] nodal_indices = [] - for station in asset_topology.stations: + for station in asset_topology.materialize_stations(): if station.grid_model_id in outage_station_busbars_map: for bb_id in outage_station_busbars_map[station.grid_model_id]: (branch_indices_to_outage, nodal_injection_to_outage, node_index_to_outage) = extract_busbar_outage_data( @@ -515,7 +515,7 @@ def get_branch_injection_outages_for_rel_subs( def get_modified_stations( network_data: NetworkData, stations_to_outage: Optional[list[str]], -) -> list[Optional[list[Station]]]: +) -> list[Optional[list[MaterializedStation]]]: """Get the modified stations after applying branch actions. Note: We don't consider injection actions here. @@ -551,7 +551,7 @@ def get_modified_stations( def get_all_rel_bb_outage_data( - modified_stations: list[Optional[list[Station]]], + modified_stations: list[Optional[list[MaterializedStation]]], network_data: NetworkData, busbars_to_outage: Optional[set[str]] = None, ) -> list[Optional[list[list[Optional[OutageData]]]]]: @@ -744,7 +744,7 @@ def get_non_rel_articulation_nodes( , list[str]],network_data: NetworkData) -> dict[str, list[str]]: """ asset_topology = network_data.simplified_asset_topology - for station in asset_topology.stations: + for station in asset_topology.materialize_stations(): if station.grid_model_id in non_rel_busbar_outage_map: busbar_intid_index_mapper = {busbar.int_id: index for index, busbar in enumerate(station.busbars)} nodes = [busbar_index for busbar_index in range(len(station.busbars))] @@ -768,7 +768,7 @@ def get_non_rel_articulation_nodes( def get_rel_articulation_nodes( - rel_stations: list[Station], busbar_a_mappings: list[list[list[int]]] + rel_stations: list[RawStation | MaterializedStation], busbar_a_mappings: list[list[list[int]]] ) -> list[list[list[int]]]: """Determine the busbars that serve as articulation nodes for relevant substations in the network. @@ -780,7 +780,7 @@ def get_rel_articulation_nodes( Parameters ---------- - rel_stations : list[Station] + rel_stations : list[RawStation | MaterializedStation] A list of relevant substations in the network. busbar_a_mappings : list[list[list[int]]] A list of busbar_a mappings for each relevant substation. The outer list is of length equal to the number diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py index 38185eb35..1050af6a5 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py @@ -19,7 +19,7 @@ NetworkData, get_relevant_stations, ) -from toop_engine_interfaces.asset_topology import Station +from toop_engine_interfaces.asset_topology import RawStation from toop_engine_interfaces.asset_topology_helpers import get_connected_assets from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits @@ -77,7 +77,7 @@ def enumerate_station_realisations( for index, (station, local_branch_action_set, separation_set_info) in enumerate( zip( - network_data.simplified_asset_topology.stations, + network_data.simplified_asset_topology.materialize_stations(), branch_action_set, network_data.separation_sets_info, strict=True, @@ -111,7 +111,7 @@ def enumerate_station_realisations( def get_injections_on_physical_bb( - network_data: NetworkData, sub: Station, busbar_index: int + network_data: NetworkData, sub: RawStation, busbar_index: int ) -> Float[np.ndarray, " n_timesteps"]: """Calculate the total connected injections in megawatts (MW) to a given physical busbar inside a substation. @@ -129,7 +129,7 @@ def get_injections_on_physical_bb( Float[np.ndarray, " n_timesteps"] The total connected injections in MW for the specified busbar index over all timesteps. """ - connected_assets = get_connected_assets(sub, busbar_index) + connected_assets = get_connected_assets(sub, busbar_index, network_data.simplified_asset_topology.assets) connected_injection_ids = [ asset.grid_model_id for asset in connected_assets if asset.in_service and not asset.is_branch() ] @@ -148,7 +148,7 @@ def get_injections_on_physical_bb( def get_injections_on_electrical_busbar( - network_data: NetworkData, sub: Station, busbar_mapping: list[int] + network_data: NetworkData, sub: RawStation, busbar_mapping: list[int] ) -> Float[np.ndarray, " n_timesteps"]: """Calculate the total injections on an electrical busbar. diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py index d3384c782..c18996036 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py @@ -27,9 +27,10 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, - Station, + MaterializedStation, SwitchableAsset, Topology, + copy_topology_with_updates, ) from toop_engine_interfaces.asset_topology_helpers import ( filter_disconnected_busbars, @@ -70,7 +71,7 @@ class OptimalSeparationSetInfo: def make_separation_set( - station: Station, + station: MaterializedStation, ) -> tuple[ Bool[np.ndarray, " n_configurations 2 ..."], Bool[np.ndarray, " n_configurations n_couplers"], @@ -259,8 +260,8 @@ def __bool__(self) -> bool: def prepare_for_separation_set( - station: Station, branch_ids: list[str], injection_ids: list[str], close_couplers: bool = False -) -> tuple[Station, StationProblems]: + station: MaterializedStation, branch_ids: list[str], injection_ids: list[str], close_couplers: bool = False +) -> tuple[MaterializedStation, StationProblems]: """Prepare a Station so it can be used in make_separation_set. This function will: @@ -326,13 +327,13 @@ def prepare_for_separation_set( update={"model_log": (station.model_log or []) + [f"Problems during simplification: {problems}"]} ) - Station.model_validate(station) + MaterializedStation.model_validate(station) return station, problems def make_optimal_separation_set( - station: Station, + station: MaterializedStation, clip_hamming_distance: int = 0, clip_at_size: int = 100, ) -> OptimalSeparationSetInfo: @@ -423,29 +424,36 @@ def add_missing_asset_topology_branch_info( # Faster lookup of the position of the branch branch_id_lookup = {branch_id: i for i, branch_id in enumerate(branch_ids)} - new_stations = [] - for station in asset_topology.stations: - new_assets = [] - for asset in station.assets: - index = branch_id_lookup.get(asset.grid_model_id, None) - if index is not None: - branch_end = "from" if branch_from_nodes[index] == station.grid_model_id else "to" - new_assets.append( - asset.model_copy( - update={ - "name": (branch_names[index] if overwrite_if_present or asset.name is None else asset.name), - "type": (branch_types[index] if overwrite_if_present or asset.type is None else asset.type), - "branch_end": ( - branch_end if overwrite_if_present or asset.branch_end is None else asset.branch_end - ), - } - ) - ) + updated_assets_by_id: dict[str, SwitchableAsset] = {} + for asset in asset_topology.assets: + index = branch_id_lookup.get(asset.grid_model_id, None) + if index is not None: + updated_assets_by_id[asset.grid_model_id] = asset.model_copy( + update={ + "name": (branch_names[index] if overwrite_if_present or asset.name is None else asset.name), + "type": (branch_types[index] if overwrite_if_present or asset.type is None else asset.type), + } + ) + else: + updated_assets_by_id[asset.grid_model_id] = asset + + new_raw_stations = [] + for station in asset_topology.raw_stations: + new_branch_ends = [] + for asset_id, branch_end in zip(station.asset_ids, station.asset_branch_ends, strict=True): + index = branch_id_lookup.get(asset_id, None) + if index is not None and (overwrite_if_present or branch_end is None): + new_branch_ends.append("from" if branch_from_nodes[index] == station.grid_model_id else "to") else: - new_assets.append(asset) - new_stations.append(station.model_copy(update={"assets": new_assets})) - - return asset_topology.model_copy(update={"stations": new_stations}) + new_branch_ends.append(branch_end) + new_raw_stations.append(station.model_copy(update={"asset_branch_ends": new_branch_ends})) + + return copy_topology_with_updates( + reference_topology=asset_topology, + raw_stations=new_raw_stations, + assets=[updated_assets_by_id[asset.grid_model_id] for asset in asset_topology.assets], + asset_bays=asset_topology.asset_bays, + ) def add_missing_asset_topology_injection_info( @@ -482,22 +490,24 @@ def add_missing_asset_topology_injection_info( # Faster lookup of the position of the injection injection_id_lookup = {injection_id: i for i, injection_id in enumerate(injection_ids)} - new_stations = [] - for station in asset_topology.stations: - new_assets = [] - for asset in station.assets: - index = injection_id_lookup.get(asset.grid_model_id, None) - if index is not None: - new_assets.append( - asset.model_copy( - update={ - "name": (injection_names[index] if overwrite_if_present or asset.name is None else asset.name), - "type": (injection_types[index] if overwrite_if_present or asset.type is None else asset.type), - } - ) + updated_assets = [] + for asset in asset_topology.assets: + index = injection_id_lookup.get(asset.grid_model_id, None) + if index is not None: + updated_assets.append( + asset.model_copy( + update={ + "name": (injection_names[index] if overwrite_if_present or asset.name is None else asset.name), + "type": (injection_types[index] if overwrite_if_present or asset.type is None else asset.type), + } ) - else: - new_assets.append(asset) - new_stations.append(station.model_copy(update={"assets": new_assets})) - - return asset_topology.model_copy(update={"stations": new_stations}) + ) + else: + updated_assets.append(asset) + + return copy_topology_with_updates( + reference_topology=asset_topology, + raw_stations=asset_topology.raw_stations, + assets=updated_assets, + asset_bays=asset_topology.asset_bays, + ) diff --git a/packages/dc_solver_pkg/tests/conftest.py b/packages/dc_solver_pkg/tests/conftest.py index 31893bdf7..1f1394982 100644 --- a/packages/dc_solver_pkg/tests/conftest.py +++ b/packages/dc_solver_pkg/tests/conftest.py @@ -97,7 +97,7 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, - Station, + MaterializedStation, SwitchableAsset, Topology, ) @@ -631,7 +631,7 @@ def oberrhein_outage_station_busbars_map(_oberrhein_data_folder: Path) -> dict: asset_topo = load_asset_topology(_oberrhein_data_folder / PREPROCESSING_PATHS["asset_topology_file_path"]) retval = {} - for station in asset_topo.stations: + for station in asset_topo.materialize_stations(): if station.grid_model_id in stations_desired: # Get the busbar IDs for the station busbars = [bb.grid_model_id for bb in station.busbars] @@ -841,7 +841,7 @@ def basic_node_breaker_topology() -> Topology: @pytest.fixture(scope="session") -def mock_station() -> Station: +def mock_station() -> MaterializedStation: asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, branch_end="from", type="line") asset2 = SwitchableAsset(grid_model_id="branch_02", in_service=True, branch_end="to", type="line") asset3 = SwitchableAsset(grid_model_id="branch_03", in_service=True, branch_end="from", type="line") @@ -859,7 +859,7 @@ def mock_station() -> Station: # 1-2-3-4-5 # Create a mock Station object - station = Station( + station = MaterializedStation( grid_model_id="station_1", busbars=[busbar_0, busbar_1, busbar_2, busbar_3, busbar_4], couplers=[ diff --git a/packages/dc_solver_pkg/tests/deprecated/assignment.py b/packages/dc_solver_pkg/tests/deprecated/assignment.py index cf001b154..021315bb9 100644 --- a/packages/dc_solver_pkg/tests/deprecated/assignment.py +++ b/packages/dc_solver_pkg/tests/deprecated/assignment.py @@ -19,7 +19,7 @@ from toop_engine_dc_solver.preprocess.preprocess_switching import ( make_separation_set, ) -from toop_engine_interfaces.asset_topology import Station +from toop_engine_interfaces.asset_topology import MaterializedStation # TODO: A different rule could take precedent here. I would first check for parallel lines/trafos @@ -145,13 +145,13 @@ def handle_busbar(should_be_on: bool, is_on: bool, busbar: list[int]) -> None: def realize_single_station_assignment( - station: Station, + station: MaterializedStation, configuration_table: Bool[np.ndarray, " n_configurations 2 n_assets"], coupler_states: Bool[np.ndarray, " n_configurations n_couplers"], busbar_matchings: list[set[int]], target_configuration: Bool[np.ndarray, " n_assets"], target_ignores: Optional[Bool[np.ndarray, " n_assets"]], -) -> tuple[Optional[Station], list[int], int]: +) -> tuple[Optional[MaterializedStation], list[int], int]: """Realize a electric target to be physically implemented in the station. This fundamentally involves two steps, at first finding the closest busbar mapping and then @@ -265,7 +265,7 @@ def realize_single_station_assignment( "couplers": new_couplers, } ) - Station.model_validate(new_station) + MaterializedStation.model_validate(new_station) return new_station, busbar_a, int(reassignment_distance) @@ -275,9 +275,9 @@ def realise_bus_split_single_station( branch_topology_local: Bool[ArrayLike, " n_branches_at_node"], injection_ids_local: list[str], injection_topology_local: Bool[np.ndarray, " n_injections_at_node"], - station: Station, + station: MaterializedStation, missing_element_behavior: Literal["raise", "leave"] = "leave", -) -> tuple[Optional[Station], list[int], int]: +) -> tuple[Optional[MaterializedStation], list[int], int]: """Realizes the bus split for a single station. In this method, the switching table of the input Station is modified as per input diff --git a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py index 05bd3df1c..7621f6408 100644 --- a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py +++ b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py @@ -141,7 +141,7 @@ def test_get_coupler_states_from_busbar_couplers(): def test_get_asset_switch_states_from_station(basic_node_breaker_topology): - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) expected_reassignment = [ {"grid_model_id": "L42_DISCONNECTOR_3_0", "open": True}, @@ -190,7 +190,7 @@ def test_get_asset_switch_states_from_station(basic_node_breaker_topology): def test_get_asset_bay_sr_fid_list(basic_node_breaker_topology): - station = deepcopy(basic_node_breaker_topology.stations[0]) + station = deepcopy(basic_node_breaker_topology.materialize_stations()[0]) asset_bay_sr_fid_list = get_asset_bay_grid_model_id_list(station) expected = [ {"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, @@ -214,14 +214,14 @@ def test_get_asset_bay_sr_fid_list(basic_node_breaker_topology): def test_get_busbar_lookup(basic_node_breaker_topology): - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] busbar_lookup = get_busbar_lookup(station) expected = {0: "BBS4_1", 1: "BBS4_2"} assert busbar_lookup == expected def test_station_helpers_build_switch_update_schema(basic_node_breaker_topology): - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) switch_update_schema = cast( @@ -243,7 +243,7 @@ def test_station_helpers_build_switch_update_schema(basic_node_breaker_topology) def test_get_diff_switch_states(basic_node_breaker_grid_v1, basic_node_breaker_topology): net = basic_node_breaker_grid_v1 - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) switch_update_schema = cast( @@ -277,7 +277,7 @@ def test_get_changing_switches_from_topology(basic_node_breaker_grid_v1, basic_n def test_switch_update_schema_to_dgs(basic_node_breaker_grid_v1, basic_node_breaker_topology): _net = basic_node_breaker_grid_v1 - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) switch_update_schema = cast( diff --git a/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py index 4bcd1dfbe..fcf6b711e 100644 --- a/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py @@ -14,10 +14,34 @@ ) from toop_engine_dc_solver.export.export import get_changing_switches_from_actions from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import get_changing_switches_from_topology +from toop_engine_interfaces.asset_topology import RawStation, Topology, copy_topology_with_updates from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema +def build_test_topology(reference_topology: Topology, raw_stations: list[RawStation]) -> Topology: + """Build a topology copy from raw stations for disconnection tests. + + Parameters + ---------- + reference_topology : Topology + Source topology providing shared metadata and topology-owned payloads. + raw_stations : list[RawStation] + Raw stations to place into the copied topology. + + Returns + ------- + Topology + Topology copy with updated raw stations. + """ + return copy_topology_with_updates( + reference_topology=reference_topology, + raw_stations=raw_stations, + assets=reference_topology.assets, + asset_bays=reference_topology.asset_bays, + ) + + def test_get_disconnected_asset_ids_returns_asset_bays_by_disconnection_id(basic_node_breaker_topology) -> None: disconnections = [ GridElement(id="L8", name="", type="LINE", kind="branch"), @@ -25,7 +49,7 @@ def test_get_disconnected_asset_ids_returns_asset_bays_by_disconnection_id(basic ] result = get_disconnected_asset_ids( - stations=basic_node_breaker_topology.stations, + stations=basic_node_breaker_topology.materialize_stations(), disconnections=disconnections, ) @@ -37,7 +61,7 @@ def test_get_disconnected_asset_ids_returns_asset_bays_by_disconnection_id(basic def test_get_changing_switches_from_actions_warns_for_unrepresentable_disconnection( basic_node_breaker_topology, ) -> None: - starting_topology = basic_node_breaker_topology.model_copy(update={"stations": []}) + starting_topology = build_test_topology(basic_node_breaker_topology, []) disconnections = [GridElement(id="L8", name="Line 8", type="LINE", kind="branch")] with patch("toop_engine_dc_solver.export.disconnection_switch_updates.logger.warning") as warning_mock: @@ -63,15 +87,22 @@ def test_get_changing_switches_from_disconnections_matches_network_diff( basic_node_breaker_topology, ) -> None: net = basic_node_breaker_grid_v1 - target_station = basic_node_breaker_topology.stations[0] + target_station = basic_node_breaker_topology.materialize_stations()[0] starting_station = target_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_station.couplers], "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) - target_topology = basic_node_breaker_topology.model_copy(update={"stations": [target_station]}) - starting_topology = target_topology.model_copy(update={"stations": [starting_station]}) + target_raw_station = basic_node_breaker_topology.raw_stations[0] + starting_raw_station = target_raw_station.model_copy( + update={ + "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_raw_station.couplers], + "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + } + ) + target_topology = build_test_topology(basic_node_breaker_topology, [target_raw_station]) + starting_topology = build_test_topology(target_topology, [starting_raw_station]) disconnections = [GridElement(id="L8", name="", type="LINE", kind="branch")] expected = get_changing_switches_from_topology(network=net, target_topology=target_topology) @@ -88,7 +119,7 @@ def test_get_changing_switches_from_disconnections_matches_network_diff( def test_get_changing_switches_from_actions_warns_on_overlapping_switch_updates( basic_node_breaker_topology, ) -> None: - target_station = basic_node_breaker_topology.stations[0] + target_station = basic_node_breaker_topology.materialize_stations()[0] changed_station = target_station.model_copy( update={ "asset_switching_table": np.array([[False, False, False], [True, True, False]], dtype=bool), @@ -100,7 +131,16 @@ def test_get_changing_switches_from_actions_warns_on_overlapping_switch_updates( "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) - starting_topology = basic_node_breaker_topology.model_copy(update={"stations": [starting_station]}) + starting_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( + update={ + "couplers": [ + coupler.model_copy(update={"open": False}) + for coupler in basic_node_breaker_topology.raw_stations[0].couplers + ], + "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + } + ) + starting_topology = build_test_topology(basic_node_breaker_topology, [starting_raw_station]) disconnections = [GridElement(id="L8", name="", type="LINE", kind="branch")] with patch("toop_engine_dc_solver.export.export.logger.warning") as warning_mock: diff --git a/packages/dc_solver_pkg/tests/export/test_export.py b/packages/dc_solver_pkg/tests/export/test_export.py index a76140ed8..1a6d095a9 100644 --- a/packages/dc_solver_pkg/tests/export/test_export.py +++ b/packages/dc_solver_pkg/tests/export/test_export.py @@ -21,31 +21,62 @@ from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import get_changing_switches_from_topology from toop_engine_dc_solver.postprocess.postprocess_powsybl import PowsyblRunner from toop_engine_dc_solver.preprocess.network_data import extract_action_set, extract_nminus1_definition, load_lf_params +from toop_engine_interfaces.asset_topology import RawStation, Topology, copy_topology_with_updates from toop_engine_interfaces.folder_structure import OUTPUT_FILE_NAMES, POSTPROCESSING_PATHS, PREPROCESSING_PATHS from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.stored_action_set import ActionSet from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema +def build_test_topology(reference_topology: Topology, raw_stations: list[RawStation]) -> Topology: + """Build a topology copy from raw stations for export tests. + + Parameters + ---------- + reference_topology : Topology + Source topology providing shared metadata and topology-owned payloads. + raw_stations : list[RawStation] + Raw stations to place into the copied topology. + + Returns + ------- + Topology + Topology copy with updated raw stations. + """ + return copy_topology_with_updates( + reference_topology=reference_topology, + raw_stations=raw_stations, + assets=reference_topology.assets, + asset_bays=reference_topology.asset_bays, + ) + + def test_get_changing_switches_from_actions_matches_network_diff( basic_node_breaker_grid_v1, basic_node_breaker_topology, ): net = basic_node_breaker_grid_v1 - target_station = basic_node_breaker_topology.stations[0] + target_station = basic_node_breaker_topology.materialize_stations()[0] changed_station = target_station.model_copy( update={ "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) + target_raw_station = basic_node_breaker_topology.raw_stations[0] starting_station = target_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_station.couplers], "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) - target_topology = basic_node_breaker_topology.model_copy(update={"stations": [target_station]}) - starting_topology = target_topology.model_copy(update={"stations": [starting_station]}) + starting_raw_station = target_raw_station.model_copy( + update={ + "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_raw_station.couplers], + "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + } + ) + target_topology = build_test_topology(basic_node_breaker_topology, [target_raw_station]) + starting_topology = build_test_topology(target_topology, [starting_raw_station]) disconnections = [GridElement(id="L8", name="", type="LINE", kind="branch")] expected = get_changing_switches_from_topology(network=net, target_topology=target_topology) @@ -63,19 +94,23 @@ def test_get_changing_switches_from_actions_matches_network_diff( def test_get_changing_switches_from_action_set_matches_expanded_inputs( basic_node_breaker_topology, ) -> None: - target_station = basic_node_breaker_topology.stations[0] + target_station = basic_node_breaker_topology.materialize_stations()[0] changed_station = target_station.model_copy( update={ "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) - starting_station = target_station.model_copy( + + starting_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( update={ - "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_station.couplers], + "couplers": [ + coupler.model_copy(update={"open": False}) + for coupler in basic_node_breaker_topology.raw_stations[0].couplers + ], "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) - starting_topology = basic_node_breaker_topology.model_copy(update={"stations": [starting_station]}) + starting_topology = build_test_topology(basic_node_breaker_topology, [starting_raw_station]) disconnection_elements = [GridElement(id="L8", name="", type="LINE", kind="branch")] action_set = ActionSet.model_construct( starting_topology=starting_topology, @@ -125,7 +160,7 @@ def test_get_changing_switches_from_action_set_validates_indices( disconnectable_branches=[GridElement(id="L8", name="", type="LINE", kind="branch")], pst_ranges=[], hvdc_ranges=[], - local_actions=[basic_node_breaker_topology.stations[0]], + local_actions=[basic_node_breaker_topology.materialize_stations()[0]], ) with pytest.raises(ValueError, match=expected_message): diff --git a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py index 816152fbe..9c4886f1e 100644 --- a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py @@ -14,11 +14,35 @@ get_changing_switches_from_changed_stations, ) from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import get_changing_switches_from_topology +from toop_engine_interfaces.asset_topology import RawStation, Topology, copy_topology_with_updates from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema +def build_test_topology(reference_topology: Topology, raw_stations: list[RawStation]) -> Topology: + """Build a topology copy from raw stations for station-switch tests. + + Parameters + ---------- + reference_topology : Topology + Source topology providing shared metadata and topology-owned payloads. + raw_stations : list[RawStation] + Raw stations to place into the copied topology. + + Returns + ------- + Topology + Topology copy with updated raw stations. + """ + return copy_topology_with_updates( + reference_topology=reference_topology, + raw_stations=raw_stations, + assets=reference_topology.assets, + asset_bays=reference_topology.asset_bays, + ) + + def test_resolve_changed_stations_preserves_topology_order(basic_node_breaker_topology): - changed_station = basic_node_breaker_topology.stations[0] + changed_station = basic_node_breaker_topology.materialize_stations()[0] starting_lookup, changed_lookup, ordered_station_ids = _resolve_changed_stations( changed_stations=[changed_station], @@ -31,14 +55,11 @@ def test_resolve_changed_stations_preserves_topology_order(basic_node_breaker_to def test_get_coupler_switch_diffs(basic_node_breaker_topology): - starting_station = basic_node_breaker_topology.stations[0].model_copy( - update={ - "couplers": [ - coupler.model_copy(update={"open": False}) for coupler in basic_node_breaker_topology.stations[0].couplers - ] - } + station = basic_node_breaker_topology.materialize_stations()[0] + starting_station = station.model_copy( + update={"couplers": [coupler.model_copy(update={"open": False}) for coupler in station.couplers]} ) - changed_station = basic_node_breaker_topology.stations[0] + changed_station = station result = _get_coupler_switch_diffs( changed_station=changed_station, @@ -49,7 +70,7 @@ def test_get_coupler_switch_diffs(basic_node_breaker_topology): def test_get_asset_switch_diffs(basic_node_breaker_topology): - target_station = basic_node_breaker_topology.stations[0] + target_station = basic_node_breaker_topology.materialize_stations()[0] starting_station = target_station.model_copy( update={ "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), @@ -73,7 +94,7 @@ def test_get_asset_switch_diffs(basic_node_breaker_topology): def test_get_asset_switch_diffs_requires_matching_switching_table_shape(basic_node_breaker_topology): - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] changed_station = station.model_copy( update={ "asset_switching_table": np.array([[False, False, True]], dtype=bool), @@ -88,7 +109,7 @@ def test_get_asset_switch_diffs_requires_matching_switching_table_shape(basic_no def test_get_asset_switch_diffs_requires_matching_asset_order(basic_node_breaker_topology): - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] reordered_assets = [station.assets[1], station.assets[0], station.assets[2]] changed_station = station.model_copy(update={"assets": reordered_assets}) @@ -100,7 +121,7 @@ def test_get_asset_switch_diffs_requires_matching_asset_order(basic_node_breaker def test_get_asset_switch_diffs_allows_multiple_active_busbars(basic_node_breaker_topology): - station = basic_node_breaker_topology.stations[0] + station = basic_node_breaker_topology.materialize_stations()[0] starting_station = station.model_copy( update={ "asset_switching_table": np.array([[True, False, False], [True, True, False]], dtype=bool), @@ -125,11 +146,10 @@ def test_get_changing_switches_from_changed_stations_matches_network_diff( basic_node_breaker_topology, ): net = basic_node_breaker_grid_v1 - starting_station = basic_node_breaker_topology.stations[0].model_copy( + station = basic_node_breaker_topology.materialize_stations()[0] + starting_station = station.model_copy( update={ - "couplers": [ - coupler.model_copy(update={"open": False}) for coupler in basic_node_breaker_topology.stations[0].couplers - ], + "couplers": [coupler.model_copy(update={"open": False}) for coupler in station.couplers], "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) @@ -139,8 +159,25 @@ def test_get_changing_switches_from_changed_stations_matches_network_diff( "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) - target_topology = basic_node_breaker_topology.model_copy(update={"stations": [changed_station]}) - starting_topology = target_topology.model_copy(update={"stations": [starting_station]}) + target_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( + update={ + "couplers": [ + coupler.model_copy(update={"open": True}) for coupler in basic_node_breaker_topology.raw_stations[0].couplers + ], + "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), + } + ) + starting_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( + update={ + "couplers": [ + coupler.model_copy(update={"open": False}) + for coupler in basic_node_breaker_topology.raw_stations[0].couplers + ], + "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + } + ) + target_topology = build_test_topology(basic_node_breaker_topology, [target_raw_station]) + starting_topology = build_test_topology(target_topology, [starting_raw_station]) expected = get_changing_switches_from_topology(network=net, target_topology=target_topology) result = get_changing_switches_from_changed_stations( 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..855e3e2a8 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,487 @@ -{ - "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": "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, + "raw_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": null + }, + { + "grid_model_id": "BBS2_2", + "type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": null + }, + { + "grid_model_id": "BBS2_3", + "type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": null + } + ], + "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 + } + ], + "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": [], + "asset_ids": [ + "L1", + "L2", + "L6", + "L7", + "L8", + "load1" + ], + "asset_branch_ends": [ + null, + null, + null, + null, + null, + null + ], + "asset_bay_ids": [ + "VL2_0::L1::bay", + "VL2_0::L2::bay", + "VL2_0::L6::bay", + "VL2_0::L7::bay", + "VL2_0::L8::bay", + "VL2_0::load1::bay" + ] + }, + { + "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": null + }, + { + "grid_model_id": "BBS3_2", + "type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": null + }, + { + "grid_model_id": "BBS3_3", + "type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": null + }, + { + "grid_model_id": "BBS3_4", + "type": "busbar", + "name": "bus4", + "int_id": 3, + "in_service": true, + "bus_branch_bus_id": null + } + ], + "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 + } + ], + "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_ids": [ + "L3", + "L6", + "L7", + "L9", + "load2" + ], + "asset_branch_ends": [ + null, + null, + null, + null, + null + ], + "asset_bay_ids": [ + "VL3_0::L3::bay", + "VL3_0::L6::bay", + "VL3_0::L7::bay", + "VL3_0::L9::bay", + "VL3_0::load2::bay" + ] + } + ], + "assets": [ + { + "grid_model_id": "L1", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L2", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L6", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L7", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L8", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "load1", + "type": "LOAD", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L3", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "L9", + "type": "LINE", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + }, + { + "grid_model_id": "load2", + "type": "LOAD", + "name": "", + "in_service": true, + "branch_end": null, + "asset_bay": null, + "asset_bay_id": null + } + ], + "asset_bays": [ + { + "asset_bay_id": "VL2_0::L1::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" + } + }, + { + "asset_bay_id": "VL2_0::L2::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" + } + }, + { + "asset_bay_id": "VL2_0::L6::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" + } + }, + { + "asset_bay_id": "VL2_0::L7::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" + } + }, + { + "asset_bay_id": "VL2_0::L8::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" + } + }, + { + "asset_bay_id": "VL2_0::load1::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_bay_id": "VL3_0::L3::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" + } + }, + { + "asset_bay_id": "VL3_0::L6::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" + } + }, + { + "asset_bay_id": "VL3_0::L7::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" + } + }, + { + "asset_bay_id": "VL3_0::L9::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" + } + }, + { + "asset_bay_id": "VL3_0::load2::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_setpoints": null, + "timestamp": "2025-05-16T15:25:10.814573", + "metrics": null +} 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..f09e346e7 100644 --- a/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py +++ b/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py @@ -15,7 +15,6 @@ import pypowsybl as pp import pytest 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 from toop_engine_dc_solver.jax.busbar_outage import ( filter_already_outaged_branches_single_outage, @@ -46,7 +45,7 @@ ) 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.network_data import extract_action_set from toop_engine_dc_solver.preprocess.preprocess import NetworkData from toop_engine_dc_solver.preprocess.preprocess_bb_outage import ( extract_busbar_outage_data, @@ -54,12 +53,11 @@ get_busbar_index, get_connected_assets, get_rel_non_rel_sub_bb_maps, - get_relevant_stations, get_total_injection_along_stub_branch, logger, preprocess_bb_outages, ) -from toop_engine_interfaces.asset_topology import Station +from toop_engine_interfaces.asset_topology import MaterializedStation # FIxme: Look deeply into this @@ -104,7 +102,7 @@ def test_perform_outage_single_busbar( connected_branches_data = {} for node_index in node_index_busbars: station_id = network_data_preprocessed.node_ids[node_index] - for station in network_data_preprocessed.asset_topology.stations: + for station in network_data_preprocessed.asset_topology.materialize_stations(): if station_id == station.grid_model_id: for index, busbar in enumerate(station.busbars): connected_assets = get_connected_assets(station, index) @@ -118,7 +116,7 @@ def test_perform_outage_single_busbar( # bubsars_to_be_outaged = [busbar for busbars in list(non_rel_station_busbars_map.values()) for busbar in busbars] sorted_busbars_to_be_outaged = [] - for station in network_data_preprocessed.asset_topology.stations: + for station in network_data_preprocessed.asset_topology.materialize_stations(): if station.grid_model_id in non_rel_station_busbars_map: sorted_busbars_to_be_outaged += non_rel_station_busbars_map[station.grid_model_id] @@ -200,7 +198,7 @@ def test_perform_outage_single_busbar_with_disconnections( connected_branches_data = {} for node_index in node_index_busbars: station_id = network_data_preprocessed.node_ids[node_index] - for station in network_data_preprocessed.asset_topology.stations: + for station in network_data_preprocessed.asset_topology.materialize_stations(): if station_id == station.grid_model_id: for index, busbar in enumerate(station.busbars): connected_assets = get_connected_assets(station, index) @@ -214,7 +212,7 @@ def test_perform_outage_single_busbar_with_disconnections( # bubsars_to_be_outaged = [busbar for busbars in list(non_rel_station_busbars_map.values()) for busbar in busbars] sorted_busbars_to_be_outaged = [] - for station in network_data_preprocessed.asset_topology.stations: + for station in network_data_preprocessed.asset_topology.materialize_stations(): if station.grid_model_id in non_rel_station_busbars_map: sorted_busbars_to_be_outaged += non_rel_station_busbars_map[station.grid_model_id] @@ -337,7 +335,9 @@ def test_perform_rel_bb_outage_single_topo_with_no_inj_reassignments( validate_zero_flows(lfs, success, busbar_data["busbar_br_outage_map"]) -def get_busbar_injection_map(station: Station, network: NetworkData) -> dict[str, Float[np.ndarray, " n_timestep"]]: +def get_busbar_injection_map( + station: MaterializedStation, network: NetworkData +) -> dict[str, Float[np.ndarray, " n_timestep"]]: busbar_injection_map = {} for i, bb in enumerate(station.busbars): connected_assets = get_connected_assets(station, i) @@ -356,22 +356,14 @@ def get_busbar_injection_map(station: Station, network: NetworkData) -> dict[str def extract_busbar_metadata_after_split(network_data_preprocessed, static_information: StaticInformation, branch_actions): - rel_stations = get_relevant_stations(network_data_preprocessed) - branch_ids_mapped, _ = map_branch_injection_ids(network_data_preprocessed) + assert network_data_preprocessed.realised_stations is not None busbar_branches_map = {} busbar_deltap_map = {} busbar_br_outage_map = {} busbar_injection_map = {} - for sub_index in range(len(rel_stations)): + for sub_index in range(len(network_data_preprocessed.realised_stations)): branch_action = branch_actions[sub_index] - modified_station_br, _, _ = realise_bus_split_single_station( - branch_ids_local=branch_ids_mapped[sub_index], - branch_topology_local=branch_action[: static_information.solver_config.branches_per_sub.val[sub_index]], - injection_ids_local=[], - injection_topology_local=np.array([], dtype=bool), - station=rel_stations[sub_index], - ) branch_action_combi_index = np.argmax( np.all( network_data_preprocessed.branch_action_set[sub_index] @@ -379,6 +371,7 @@ def extract_busbar_metadata_after_split(network_data_preprocessed, static_inform axis=1, ) ) + modified_station_br = network_data_preprocessed.realised_stations[sub_index][branch_action_combi_index] for bb in modified_station_br.busbars: outage_data = extract_busbar_outage_data( modified_station_br, bb.grid_model_id, network_data_preprocessed, {}, branch_action_combi_index @@ -499,7 +492,7 @@ def test_compare_loadflows_non_rel_bb_outage_powsybl( branches_monitored=jnp.arange(dynamic_information.ptdf.shape[0]), ) lfs_index = 0 - for station in asset_topology.stations: + for station in asset_topology.materialize_stations(): if station.grid_model_id not in non_rel_bb_outage_map: continue for bb in station.busbars: @@ -583,7 +576,7 @@ def test_compare_loadflows_rel_bb_outage( lfs_index = 0 _ = apply_topology(net, actions=topo_indices.action.tolist(), action_set=nd_action_set) - for station in asset_topology.stations: + for station in asset_topology.materialize_stations(): if station.grid_model_id not in rel_bb_outage_map: continue diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py index 22885565f..570221974 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py @@ -21,7 +21,7 @@ def test_apply_station(case14_data_folder: Path) -> None: # Make sure we have valid busbar ids # Currently only one bus exists in the station, so we expect the method to create the coupler and the missing busbar. - station = asset_topo.stations[0].model_copy() + station = asset_topo.materialize_stations()[0].model_copy() station.busbars[0].grid_model_id = f"1{SEPARATOR}bus" station.busbars[1].grid_model_id = f"15{SEPARATOR}bus" station.couplers[0].grid_model_id = f"1{SEPARATOR}switch" @@ -43,7 +43,7 @@ def test_apply_station_existing_buses(case14_data_folder: Path) -> None: with open(case14_data_folder / PREPROCESSING_PATHS["asset_topology_file_path"]) as f: asset_topo = Topology.model_validate_json(f.read()) - station = asset_topo.stations[0].model_copy() + station = asset_topo.materialize_stations()[0].model_copy() station.busbars[0].grid_model_id = f"1{SEPARATOR}bus" station.busbars[1].grid_model_id = f"15{SEPARATOR}bus" station.couplers[0].grid_model_id = f"1{SEPARATOR}switch" @@ -66,7 +66,7 @@ def test_apply_station_extra_busbar(case14_data_folder: Path) -> None: with open(case14_data_folder / PREPROCESSING_PATHS["asset_topology_file_path"]) as f: asset_topo = Topology.model_validate_json(f.read()) - station = asset_topo.stations[0].model_copy() + station = asset_topo.materialize_stations()[0].model_copy() station.busbars[0].grid_model_id = f"1{SEPARATOR}bus" station.busbars[1].grid_model_id = f"15{SEPARATOR}bus" station.couplers[0].grid_model_id = f"1{SEPARATOR}switch" diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py index f3b0a01b5..c98c7c98a 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py @@ -21,16 +21,41 @@ ) from toop_engine_grid_helpers.powsybl.example_grids import basic_node_breaker_network_powsybl from toop_engine_interfaces.asset_topology import ( + RawStation, Topology, + copy_topology_with_updates, ) from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS +def build_test_topology(reference_topology: Topology, raw_stations: list[RawStation]) -> Topology: + """Build a topology copy from raw stations for postprocessing tests. + + Parameters + ---------- + reference_topology : Topology + Source topology providing shared metadata and topology-owned payloads. + raw_stations : list[RawStation] + Raw stations to place into the copied topology. + + Returns + ------- + Topology + Topology copy with updated raw stations. + """ + return copy_topology_with_updates( + reference_topology=reference_topology, + raw_stations=raw_stations, + assets=reference_topology.assets, + asset_bays=reference_topology.asset_bays, + ) + + def test_find_asset(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) - station = topology.stations[0] + station = topology.materialize_stations()[0] bus_id = station.grid_model_id vl_id = net.get_buses().loc[bus_id]["voltage_level_id"] @@ -57,7 +82,7 @@ def test_find_asset(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: assert inj["bus_id"] == bus_id assert inj["bus_breaker_bus_id"] == bus_breaker_id - for elem in topology.stations[10].assets: + for elem in topology.materialize_stations()[10].assets: if elem.is_branch(): with pytest.raises(ValueError, match=f"Branch {elem.grid_model_id} not found in the station"): find_asset( @@ -80,8 +105,9 @@ def test_apply_single_asset_bus_branch_reassign(case14_data_with_asset_topo: tup grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) - for asset_index in range(len(topology.stations[0].assets)): - station = topology.stations[0].model_copy(deep=True) + base_station = topology.materialize_stations()[0] + for asset_index in range(len(base_station.assets)): + station = base_station.model_copy(deep=True) # Reassign the first asset to the second bus assert len(station.busbars) == 2 station.asset_switching_table[:, asset_index] = ~station.asset_switching_table[:, asset_index] @@ -115,8 +141,9 @@ def test_apply_single_asset_bus_branch_disconnect(case14_data_with_asset_topo: t grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) - for asset_index in range(len(topology.stations[0].assets)): - station = topology.stations[0].model_copy(deep=True) + base_station = topology.materialize_stations()[0] + for asset_index in range(len(base_station.assets)): + station = base_station.model_copy(deep=True) # Disconnect the first asset assert len(station.busbars) == 2 station.asset_switching_table[:, asset_index] = False @@ -140,10 +167,11 @@ def test_apply_single_asset_bus_branch_nothing(case14_data_with_asset_topo: tupl grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) - for asset_index in range(len(topology.stations[0].assets)): + base_station = topology.materialize_stations()[0] + for asset_index in range(len(base_station.assets)): status, reassignments = apply_single_asset_bus_branch( net=net, - station=topology.stations[0], + station=base_station, asset_index=asset_index, ) assert status == "nothing" @@ -155,7 +183,7 @@ def test_apply_station_bus_branch_reassign(case14_data_with_asset_topo: tuple[Pa net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) # Try do nothing - station = topology.stations[0].model_copy(deep=True) + station = topology.materialize_stations()[0].model_copy(deep=True) realized_station = apply_station_bus_branch( net=net, station=station, @@ -164,7 +192,7 @@ def test_apply_station_bus_branch_reassign(case14_data_with_asset_topo: tuple[Pa assert len(realized_station.reassignment_diff) == 0 assert len(realized_station.disconnection_diff) == 0 - realized_station_2 = apply_station(net=net, station=station) + realized_station_2 = apply_station(net=net, topology=topology, raw_station=topology.raw_stations[0]) assert realized_station_2 == realized_station # Try with disconnections and reassignments @@ -184,7 +212,7 @@ def test_apply_station_bus_branch_coupler(case14_data_with_asset_topo: tuple[Pat grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) - station = topology.stations[0].model_copy(deep=True) + station = topology.materialize_stations()[0].model_copy(deep=True) station.asset_switching_table = np.array([[True, False, True], [False, True, False]]) station.couplers[0].open = True @@ -221,7 +249,7 @@ def test_apply_node_breaker_topology(basic_node_breaker_topology: Topology) -> N assert sw.loc["L82_BREAKER", "open"] net = basic_node_breaker_network_powsybl() - switch_update_df_2 = apply_station(net, basic_node_breaker_topology.stations[0]) + switch_update_df_2 = apply_station(net, basic_node_breaker_topology, basic_node_breaker_topology.raw_stations[0]) assert isinstance(switch_update_df_2, pd.DataFrame) assert switch_update_df_2.isin(switch_update_df).all().all() @@ -247,8 +275,14 @@ def test_apply_topology_bus_branch_reassign(case14_data_with_asset_topo: tuple[P # Try with disconnections and reassignments # Just randomize the switching tables, we'll have both disconnections and reassignments then - for station in topology.stations: - station.asset_switching_table = np.random.randint(0, 2, size=station.asset_switching_table.shape, dtype=bool) + raw_stations = [ + station.model_copy( + update={"asset_switching_table": np.random.randint(0, 2, size=station.asset_switching_table.shape, dtype=bool)} + ) + for station in topology.raw_stations + ] + + topology = build_test_topology(topology, raw_stations) realized_topology = apply_topology_bus_branch( net=net, diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py index ad79ba33c..98e7ad2e2 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py @@ -69,7 +69,7 @@ get_pandapower_branch_loadflow_results_sequence, ) from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import table_ids -from toop_engine_interfaces.asset_topology import Station +from toop_engine_interfaces.asset_topology import MaterializedStation, copy_topology_with_updates from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.messages.preprocess.preprocess_heartbeat import PreprocessStage @@ -585,16 +585,23 @@ def test_enumerate_station_realisations( for stations, br_act in zip(network_data.realised_stations, network_data.branch_action_set, strict=True): assert br_act.shape[0] == len(stations) for station in stations: - Station.model_validate(station) + MaterializedStation.model_validate(station) def test_enumerate_station_realisations_no_coupler( network_data_filled: NetworkData, ) -> None: - stations = [station.model_copy(update={"couplers": []}) for station in network_data_filled.asset_topology.stations] + raw_stations = [ + station.model_copy(update={"couplers": []}) for station in network_data_filled.asset_topology.raw_stations + ] network_data = replace( network_data_filled, - asset_topology=network_data_filled.asset_topology.model_copy(update={"stations": stations}), + asset_topology=copy_topology_with_updates( + reference_topology=network_data_filled.asset_topology, + raw_stations=raw_stations, + assets=network_data_filled.asset_topology.assets, + asset_bays=network_data_filled.asset_topology.asset_bays, + ), ) network_data = filter_relevant_nodes_branch_count(network_data) network_data = compute_branch_topology_info(network_data) @@ -611,12 +618,13 @@ def test_simplify_asset_topology( network_data = compute_injection_topology_info(network_data) network_data = simplify_asset_topology(network_data) assert network_data.simplified_asset_topology is not None - assert len(network_data.simplified_asset_topology.stations) == network_data.relevant_node_mask.sum() + simplified_stations = network_data.simplified_asset_topology.materialize_stations() + assert len(simplified_stations) == network_data.relevant_node_mask.sum() for rel_node_index, (rel_node_id, station) in enumerate( - zip(network_data.relevant_nodes, network_data.simplified_asset_topology.stations, strict=True) + zip(network_data.relevant_nodes, simplified_stations, strict=True) ): assert station.grid_model_id == network_data.node_ids[rel_node_id] - Station.model_validate(station) + MaterializedStation.model_validate(station) branch_ids = [network_data.branch_ids[i] for i in network_data.branches_at_nodes[rel_node_index]] inj_ids = [network_data.injection_ids[i] for i in network_data.injection_idx_at_nodes[rel_node_index]] asset_ids = [a.grid_model_id for a in station.assets] @@ -787,7 +795,7 @@ def test_filter_relevant_nodes_no_asset_station(network_data: NetworkData) -> No network_data, asset_topology=network_data.asset_topology.model_copy( update={ - "stations": network_data.asset_topology.stations[:2] # Keep only the first two stations + "raw_stations": network_data.asset_topology.raw_stations[:2] # Keep only the first two stations } ), ) @@ -874,11 +882,12 @@ def test_preprocess(data_folder: str, tmp_path: str) -> None: static_information = load_static_information(Path(tmp_path) / "test_static_information.hdf5") validate_static_information(static_information) + simplified_stations = network_data.simplified_asset_topology.materialize_stations() for rel_node_index, (rel_node_id, station) in enumerate( - zip(network_data.relevant_nodes, network_data.simplified_asset_topology.stations, strict=True) + zip(network_data.relevant_nodes, simplified_stations, strict=True) ): assert station.grid_model_id == network_data.node_ids[rel_node_id] - Station.model_validate(station) + MaterializedStation.model_validate(station) branch_ids = [network_data.branch_ids[i] for i in network_data.branches_at_nodes[rel_node_index]] inj_ids = [network_data.injection_ids[i] for i in network_data.injection_idx_at_nodes[rel_node_index]] asset_ids = [a.grid_model_id for a in station.assets] diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py index a4ca78095..03e4f1810 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py @@ -9,8 +9,7 @@ from dataclasses import replace import numpy as np -from tests.deprecated.assignment import realise_bus_split_single_station -from toop_engine_dc_solver.preprocess.network_data import NetworkData, map_branch_injection_ids +from toop_engine_dc_solver.preprocess.network_data import NetworkData from toop_engine_dc_solver.preprocess.preprocess import compute_separation_set_for_stations from toop_engine_dc_solver.preprocess.preprocess_bb_outage import ( extract_busbar_outage_data, @@ -23,12 +22,11 @@ get_non_rel_articulation_nodes, get_rel_articulation_nodes, get_rel_non_rel_sub_bb_maps, - get_relevant_stations, get_total_injection_along_stub_branch, update_network_data_with_non_rel_bb_outages, ) from toop_engine_dc_solver.preprocess.preprocess_station_realisations import enumerate_station_realisations -from toop_engine_interfaces.asset_topology import Busbar, Station, SwitchableAsset +from toop_engine_interfaces.asset_topology import Busbar, MaterializedStation, SwitchableAsset def test_get_total_injection_along_stub_branch(network_data: NetworkData): @@ -236,7 +234,7 @@ def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): grid_model_id="busbar_1", int_id=1, ) - station = Station( + station = MaterializedStation( grid_model_id="node_2", busbars=[busbar_0, busbar_1], couplers=[], @@ -327,7 +325,7 @@ def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): grid_model_id="busbar_0", int_id=0, ) - station = Station( + station = MaterializedStation( grid_model_id="node_0", busbars=[busbar_0], couplers=[], @@ -389,7 +387,7 @@ def test_update_network_data_with_non_rel_bb_outages(network_data_preprocessed: # Test case 3: Check if the branches to be outaged are valid and connected to the busbar for branch_outages, station_id in zip(updated_net_data.non_rel_bb_outage_br_indices, non_rel_bb_map): - for station in updated_net_data.asset_topology.stations: + for station in updated_net_data.asset_topology.materialize_stations(): if station.grid_model_id == station_id: break @@ -448,19 +446,11 @@ def test_get_branch_injection_outages_for_rel_subs( f"{[[len(outage_data_nodal_index[i][j]) for j in range(len(outage_data_nodal_index[i]))] for i in range(len(outage_data_nodal_index))]}" ) - rel_stations = get_relevant_stations(network_data_preprocessed) + assert network_data_preprocessed.realised_stations is not None rel_station_index = 0 - branch_ids_mapped, _ = map_branch_injection_ids(network_data_preprocessed) - branch_actions = network_data_preprocessed.branch_action_set for station_combis in outage_data_branch_indices: for combi_index, busbar_outages in enumerate(station_combis): - modified_station, _, _ = realise_bus_split_single_station( - branch_ids_local=branch_ids_mapped[rel_station_index], - branch_topology_local=branch_actions[rel_station_index][combi_index], - injection_ids_local=[], - injection_topology_local=np.array([], dtype=bool), - station=rel_stations[rel_station_index], - ) + modified_station = network_data_preprocessed.realised_stations[rel_station_index][combi_index] busbar_branches_map = get_busbar_branches_map(modified_station, network_data_preprocessed) @@ -498,7 +488,7 @@ def test_get_modified_stations(network_data_preprocessed: NetworkData): # switching_table: # array([[False, True, False, False, False, True, False], # [True, False, True, True, True, False, True]] - monitored_station = network_data_preprocessed.asset_topology.stations[156] + monitored_station = network_data_preprocessed.asset_topology.materialize_stations()[156] outage_stations = [monitored_station.grid_model_id] branch_actions_all_rel_sub = network_data_preprocessed.branch_action_set modified_stations_br = get_modified_stations(network_data=network_data_preprocessed, stations_to_outage=outage_stations) @@ -601,7 +591,7 @@ def test_get_non_rel_bridge_busbars(network_data_test_grid: NetworkData): assert non_rel_busbar_outage_map == expected_map, f"Expected {expected_map}, but got {non_rel_busbar_outage_map}" -def test_get_rel_bridge_busbars(mock_station: Station): +def test_get_rel_bridge_busbars(mock_station: MaterializedStation): articulation_nodes = get_rel_articulation_nodes([mock_station], [[[2, 3, 4]]]) assert articulation_nodes == [[[3]]], f"Expected [[[3]]], but got {articulation_nodes}" diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py index 4811088de..d93d22436 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py @@ -26,13 +26,13 @@ AssetInjectionType, Busbar, BusbarCoupler, - Station, + MaterializedStation, SwitchableAsset, ) def test_make_configurations_table(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -138,7 +138,7 @@ def test_identify_unnecessary_combinations() -> None: def test_preprocess_station() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -182,7 +182,7 @@ def test_preprocess_station() -> None: def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> None: - num_assets_before = sum(len(station.assets) for station in network_data.asset_topology.stations) + num_assets_before = sum(len(station.assets) for station in network_data.asset_topology.materialize_stations()) topo = add_missing_asset_topology_branch_info( asset_topology=network_data.asset_topology, @@ -195,7 +195,7 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No from_ends = 0 to_ends = 0 - for station in topo.stations: + for station in topo.materialize_stations(): for asset in station.assets: if asset.grid_model_id in network_data.branch_ids: assert asset.name in network_data.branch_names @@ -209,12 +209,12 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No assert from_ends > 0 assert to_ends > 0 - num_assets_after = sum(len(station.assets) for station in topo.stations) + num_assets_after = sum(len(station.assets) for station in topo.materialize_stations()) assert num_assets_before == num_assets_after def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> None: - num_assets_before = sum(len(station.assets) for station in network_data.asset_topology.stations) + num_assets_before = sum(len(station.assets) for station in network_data.asset_topology.materialize_stations()) topo = add_missing_asset_topology_injection_info( asset_topology=network_data.asset_topology, @@ -224,13 +224,13 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> overwrite_if_present=True, ) - for station in topo.stations: + for station in topo.materialize_stations(): for asset in station.assets: if asset.grid_model_id in network_data.injection_ids: assert asset.name in network_data.injection_names assert asset.type in network_data.injection_types - num_assets_after = sum(len(station.assets) for station in topo.stations) + num_assets_after = sum(len(station.assets) for station in topo.materialize_stations()) assert num_assets_before == num_assets_after # Test with overwrite_if_present=False @@ -242,7 +242,7 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> overwrite_if_present=False, ) - for station in topo.stations: + for station in topo.materialize_stations(): for asset in station.assets: if asset.grid_model_id in network_data.injection_ids: assert asset.name in network_data.injection_names @@ -256,7 +256,7 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> overwrite_if_present=True, ) - for station in topo.stations: + for station in topo.materialize_stations(): for asset in station.assets: if asset.grid_model_id in network_data.injection_ids: assert asset.name == "new_name" @@ -266,7 +266,7 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> def test_prepare_for_separation_set_node_breaker_test_station(): file = Path(__file__).parent.parent / "files" / "test_station.json" with open(file, "r") as f: - station = Station.model_validate_json(f.read()) + station = MaterializedStation.model_validate_json(f.read()) x = nx.Graph() for busbar in station.busbars: diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py b/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py index 31a717626..6ce268384 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py @@ -16,11 +16,11 @@ from toop_engine_dc_solver.preprocess.preprocess_switching import ( make_optimal_separation_set, ) -from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, Station, SwitchableAsset +from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, MaterializedStation, SwitchableAsset def test_realize_ba_to_physical_topo_per_station_simple(): - station = Station( + station = MaterializedStation( grid_model_id="teststation", busbars=[Busbar(grid_model_id="BB1", int_id=1), Busbar(grid_model_id="BB2", int_id=2)], couplers=[BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False)], @@ -122,7 +122,7 @@ def test_realize_ba_to_physical_topo_per_station_simple(): def test_realize_ba_to_physical_topo_per_station_3_busbars(): - station = Station( + station = MaterializedStation( grid_model_id="teststation", busbars=[ Busbar(grid_model_id="BB1", int_id=1), @@ -270,7 +270,7 @@ def test_realize_ba_to_physical_topo_per_station_large(): switching_table = np.zeros((3, n_assets), dtype=bool) switching_table[busbar_choices, range(n_assets)] = True - station = Station( + station = MaterializedStation( grid_model_id="teststation", busbars=[ Busbar(grid_model_id="BB1", int_id=1), @@ -305,7 +305,7 @@ def test_realize_ba_to_physical_topo_per_station_large(): def test_realize_ba_to_physical_topo_per_station_limited_connectivity(): - station = Station( + station = MaterializedStation( grid_model_id="teststation", busbars=[ Busbar(grid_model_id="BB1", int_id=1), @@ -395,7 +395,7 @@ def test_realize_ba_to_physical_topo_per_station_limited_connectivity(): def test_realize_ba_to_physical_topo_per_station_invalid_actions(): - station = Station( + station = MaterializedStation( grid_model_id="teststation", busbars=[ Busbar(grid_model_id="BB1", int_id=1), @@ -451,7 +451,7 @@ def test_realize_ba_to_physical_topo_per_station_invalid_actions(): @pytest.mark.xfail(reason="These edge cases are not yet handled") def test_realize_ba_to_physical_topo_per_station_invalid_actions_hard(): - station = Station( + station = MaterializedStation( grid_model_id="teststation", busbars=[ Busbar(grid_model_id="BB1", int_id=1), 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 8f4736b0e..eef975bdb 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 @@ -55,10 +55,10 @@ def test_extract_data_compare_to_network_data(network_data_preprocessed: Network n_monitored_branches = network_data_preprocessed.monitored_branch_mask.sum() n_monitored_nodes = sum( - [len(station.busbars) for station in network_data_preprocessed.simplified_asset_topology.stations] + [len(station.busbars) for station in network_data_preprocessed.simplified_asset_topology.raw_stations] ) n_monitored_switches = sum( - [len(station.couplers) for station in network_data_preprocessed.simplified_asset_topology.stations] + [len(station.couplers) for station in network_data_preprocessed.simplified_asset_topology.raw_stations] ) n_expected_monitored_elements = n_monitored_branches + n_monitored_nodes + n_monitored_switches assert n_monitored_elements == n_expected_monitored_elements, ( diff --git a/packages/dc_solver_pkg/tests/test_example_grids.py b/packages/dc_solver_pkg/tests/test_example_grids.py index 0d9e1dc30..30b96c8fb 100644 --- a/packages/dc_solver_pkg/tests/test_example_grids.py +++ b/packages/dc_solver_pkg/tests/test_example_grids.py @@ -60,7 +60,7 @@ def test_random_topology_info(data_folder: Path) -> None: ) topology = random_topology_info_backend(backend, pp_counters) - assert len(topology.stations) == sum(backend.get_relevant_node_mask()) + assert len(topology.materialize_stations()) == sum(backend.get_relevant_node_mask()) def test_oberrhein_data() -> None: @@ -481,7 +481,7 @@ def test_case14_with_matching_asset_topo() -> None: # Check the asset topology with open(tmp_dir / PREPROCESSING_PATHS["asset_topology_file_path"], "r") as f: asset_topo = Topology.model_validate_json(f.read()) - for station in asset_topo.stations: + for station in asset_topo.materialize_stations(): assert_station_in_network(backend.net, station) 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..2e6aee491 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 @@ -14,7 +14,7 @@ import pandas as pd import pypowsybl from pypowsybl.network import Network -from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import get_stations_bus_breaker +from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import get_raw_stations_and_assets_bus_breaker from toop_engine_grid_helpers.powsybl.powsybl_helpers import load_pandapower_net_for_powsybl from toop_engine_interfaces.asset_topology import Topology from toop_engine_interfaces.asset_topology_helpers import save_asset_topology @@ -660,14 +660,16 @@ def create_busbar_b_in_ieee(net: pypowsybl.network.Network) -> None: def extract_station_info_powsybl(net: Network, base_folder: Path) -> None: - stations = get_stations_bus_breaker(net) + raw_stations, topology_assets = get_raw_stations_and_assets_bus_breaker(net) target = base_folder / PREPROCESSING_PATHS["asset_topology_file_path"] target.parent.mkdir(parents=True, exist_ok=True) save_asset_topology( target, Topology( - stations=stations, topology_id="extracted_topology", + raw_stations=raw_stations, + assets=topology_assets, + asset_bays=[], timestamp=datetime.datetime.now(), ), ) diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index feeb472f1..438bf20ec 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -24,7 +24,8 @@ AssetBay, Busbar, BusbarCoupler, - Station, + MaterializedStation, + RawStation, SwitchableAsset, Topology, ) @@ -390,7 +391,7 @@ def get_relevant_network_data( def get_relevant_stations( network: Network, relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]] -) -> list[Station]: +) -> list[RawStation]: """Get all relevant stations from the network. Parameters @@ -403,8 +404,8 @@ def get_relevant_stations( Returns ------- - station: list[Station] - List of all formatted stations of the relevant buses in the network + station: list[RawStation] + List of all lean topology stations of the relevant buses in the network """ # Load relevant data once buses_with_substation_and_voltage, switches, dangling_lines, element_names = get_relevant_network_data( @@ -413,38 +414,43 @@ def get_relevant_stations( ) # Calculate the pydantic station for each relevant bus - station_list = get_list_of_stations(network, buses_with_substation_and_voltage, switches, dangling_lines, element_names) - return station_list + raw_stations, _ = get_raw_stations_and_assets( + network, buses_with_substation_and_voltage, switches, dangling_lines, element_names + ) + return raw_stations -def get_list_of_stations( +def get_raw_stations_and_assets( network: Network, buses_with_substation_and_voltage: pd.DataFrame, switches: pd.DataFrame, dangling_lines: pd.DataFrame, element_names: pd.Series, -) -> list[Station]: - """Get the list of stations from the relevant buses. +) -> tuple[list[RawStation], list[SwitchableAsset]]: + """Build raw topology stations and topology-owned assets from the relevant buses. Parameters ---------- - network: Network + network : Network pypowsybl network object - buses_with_substation_and_voltage: pd.DataFrame + buses_with_substation_and_voltage : pd.DataFrame DataFrame with the relevant buses, substation id and voltage level - switches: pd.DataFrame + switches : pd.DataFrame DataFrame with all the switches in the network. Includes the column "name" - dangling_lines: pd.DataFrame + dangling_lines : pd.DataFrame DataFrame with all the dangling lines in the network. Includes the column "tie_line_id" - element_names: pd.Series + element_names : pd.Series Series with the names of all injections and branches in the network and their ids as index Returns ------- - station_list: list[Station] - List of all formatted stations of the relevant buses in the network + station_list : list[RawStation] + List of all lean topology stations of the relevant buses in the network + topology_assets : list[SwitchableAsset] + Deduplicated topology-owned assets referenced by the stations """ - station_list = [] + station_list: list[RawStation] = [] + topology_assets: dict[str, SwitchableAsset] = {} for bus_id, bus_info in buses_with_substation_and_voltage.iterrows(): station_topology = network.get_bus_breaker_topology(bus_info.voltage_level_id) station_buses = get_bus_info_from_topology(station_topology.buses, bus_id) @@ -453,19 +459,27 @@ def get_list_of_stations( station_topology.elements, station_buses, dangling_lines, element_names ) asset_connectivity = np.ones_like(switching_matrix, dtype=bool) - station = Station( + assets = get_list_of_switchable_assets_from_df(station_elements) + for asset in assets: + topology_assets.setdefault( + asset.grid_model_id, asset.model_copy(update={"branch_end": None, "asset_bay_id": None}) + ) + + station = RawStation( grid_model_id=bus_id, name=bus_info.substation_id, region=bus_info.voltage_level_id[0:2], voltage_level=bus_info.nominal_v, busbars=get_list_of_busbars_from_df(station_buses), couplers=get_list_of_coupler_from_df(coupler_elements), - assets=get_list_of_switchable_assets_from_df(station_elements), + asset_ids=[asset.grid_model_id for asset in assets], + asset_branch_ends=[asset.branch_end for asset in assets], + asset_bay_ids=[None] * len(assets), asset_switching_table=switching_matrix, asset_connectivity=asset_connectivity, ) station_list.append(station) - return station_list + return station_list, list(topology_assets.values()) def get_topology( @@ -493,39 +507,49 @@ def get_topology( topology: Topology Topology object, including all relevant stations """ - station_list = get_relevant_stations(network=network, relevant_stations=relevant_stations) + buses_with_substation_and_voltage, switches, dangling_lines, element_names = get_relevant_network_data( + network=network, + relevant_stations=relevant_stations, + ) + raw_stations, topology_assets = get_raw_stations_and_assets( + network, buses_with_substation_and_voltage, switches, dangling_lines, element_names + ) timestamp = datetime.datetime.now() return Topology( topology_id=topology_id, grid_model_file=grid_model_file, - stations=station_list, + raw_stations=raw_stations, + assets=topology_assets, + asset_bays=[], timestamp=timestamp, ) -def get_stations_bus_breaker(net: Network) -> list[Station]: - """Convert all stations in a bus-breaker topology grid to the asset topology format. +def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStation], list[SwitchableAsset]]: + """Convert a bus-breaker topology grid to raw topology stations and topology-owned assets. - This is very similar to get_topology but only works for bus-breaker grids. This is mainly used for the test grids. - TODO find out why get_topology didn't work and remove either of the two. + This is mainly used for fixture and test-grid extraction. Parameters ---------- - net: Network + net : Network The bus/breaker powsybl network to convert Returns ------- - stations: list[Station] - List of all stations in the network + stations : list[RawStation] + List of all lean topology stations in the network + topology_assets : list[SwitchableAsset] + Deduplicated topology-owned assets referenced by the raw stations """ all_switches = net.get_switches(all_attributes=True) all_branches = net.get_branches(all_attributes=True) all_injections = net.get_injections(all_attributes=True) all_breaker_buses = net.get_bus_breaker_view_buses(all_attributes=True) - stations = [] + stations: list[RawStation] = [] + topology_assets: dict[str, SwitchableAsset] = {} for bus_id, bus_row in net.get_buses().iterrows(): local_buses = all_breaker_buses[all_breaker_buses["bus_id"] == bus_id] local_switches = all_switches[ @@ -580,22 +604,29 @@ def get_stations_bus_breaker(net: Network) -> list[Station]: for asset_index, idx in enumerate(bus_index): switching_table[idx, asset_index] = True - station = Station( + for asset in assets: + topology_assets.setdefault( + asset.grid_model_id, asset.model_copy(update={"branch_end": None, "asset_bay_id": None}) + ) + + station = RawStation( grid_model_id=bus_id, name=bus_row.name, busbars=busbars, couplers=couplers, - assets=assets, + asset_ids=[asset.grid_model_id for asset in assets], + asset_branch_ends=[asset.branch_end for asset in assets], + asset_bay_ids=[None] * len(assets), asset_switching_table=switching_table, ) stations.append(station) - return stations + return stations, list(topology_assets.values()) # TODO: refactor due to C901 def assert_station_in_network( # noqa: C901 net: Network, - station: Station, + station: MaterializedStation, couplers_strict: bool = True, assets_strict: bool = True, busbars_strict: bool = True, diff --git a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py index 4b13c1f8f..82c39fe63 100644 --- a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py @@ -23,12 +23,18 @@ get_list_of_coupler_from_df, get_list_of_switchable_assets_from_df, get_name_of_station_elements, + get_raw_stations_and_assets_bus_breaker, get_relevant_network_data, get_relevant_stations, - get_stations_bus_breaker, get_topology, ) -from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, Station, SwitchableAsset, Topology +from toop_engine_interfaces.asset_topology import ( + Busbar, + BusbarCoupler, + RawStation, + SwitchableAsset, + Topology, +) from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS @@ -341,7 +347,7 @@ def test_get_relevant_stations(ucte_file: Path): stations = get_relevant_stations(network, relevant_subs) assert len(stations) == sum(relevant_subs), "Wrong number of stations" - assert isinstance(stations[0], Station), "Wrong type of station" + assert isinstance(stations[0], RawStation), "Wrong type of station" def test_get_topology_ucte(ucte_file: Path): @@ -351,7 +357,7 @@ def test_get_topology_ucte(ucte_file: Path): topology = get_topology(network, relevant_subs, grid_model_file="booga", topology_id="wooga") assert isinstance(topology, Topology), "Wrong type of topology" - assert len(topology.stations) == sum(relevant_subs), "Wrong number of stations" + assert len(topology.materialize_stations()) == sum(relevant_subs), "Wrong number of stations" assert topology.grid_model_file == "booga" assert topology.topology_id == "wooga" @@ -389,11 +395,12 @@ def test_assert_station_in_network(case14_data_with_asset_topo: tuple[Path, Topo grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) - for station in topology.stations: + stations = topology.materialize_stations() + for station in stations: assert_station_in_network(net, station) # Change the station ID - station = topology.stations[0].model_copy(update={"grid_model_id": "hugawuga"}) + station = stations[0].model_copy(update={"grid_model_id": "hugawuga"}) with pytest.raises(ValueError, match="Station hugawuga not found in the network"): assert_station_in_network(net, station) @@ -403,11 +410,11 @@ def test_assert_station_in_network_coupler(case14_data_with_asset_topo: tuple[Pa net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) # Add a coupler to the station that is not in the grid - station = topology.stations[0].model_copy( + station = topology.materialize_stations()[0].model_copy( update={ "couplers": [ - topology.stations[0].couplers[0], - topology.stations[0].couplers[0].model_copy(update={"grid_model_id": "hugawuga"}), + topology.materialize_stations()[0].couplers[0], + topology.materialize_stations()[0].couplers[0].model_copy(update={"grid_model_id": "hugawuga"}), ], } ) @@ -416,7 +423,7 @@ def test_assert_station_in_network_coupler(case14_data_with_asset_topo: tuple[Pa assert_station_in_network(net, station) # Remove a coupler from the station - station = topology.stations[0].model_copy( + station = topology.materialize_stations()[0].model_copy( update={ "couplers": [], } @@ -432,15 +439,19 @@ def test_assert_station_in_network_busbar(case14_data_with_asset_topo: tuple[Pat net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) # Add a busbar to the station that is not in the grid - station = topology.stations[0].model_copy( + station = topology.materialize_stations()[0].model_copy( update={ "busbars": [ - topology.stations[0].busbars[0], - topology.stations[0].busbars[1], - topology.stations[0].busbars[0].model_copy(update={"grid_model_id": "hugawuga", "int_id": 3}), + topology.materialize_stations()[0].busbars[0], + topology.materialize_stations()[0].busbars[1], + topology.materialize_stations()[0].busbars[0].model_copy(update={"grid_model_id": "hugawuga", "int_id": 3}), ], "asset_switching_table": np.concatenate( - [topology.stations[0].asset_switching_table, topology.stations[0].asset_switching_table[0:1]], axis=0 + [ + topology.materialize_stations()[0].asset_switching_table, + topology.materialize_stations()[0].asset_switching_table[0:1], + ], + axis=0, ), } ) @@ -448,12 +459,12 @@ def test_assert_station_in_network_busbar(case14_data_with_asset_topo: tuple[Pat assert_station_in_network(net, station) # Remove a busbar from the station - station = topology.stations[0].model_copy( + station = topology.materialize_stations()[0].model_copy( update={ "busbars": [ - topology.stations[0].busbars[0], + topology.materialize_stations()[0].busbars[0], ], - "asset_switching_table": topology.stations[0].asset_switching_table[0:1], + "asset_switching_table": topology.materialize_stations()[0].asset_switching_table[0:1], } ) # Should pass without strict @@ -467,14 +478,18 @@ def test_assert_station_in_network_asset(case14_data_with_asset_topo: tuple[Path net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) # Add a switchable asset to the station that is not in the grid - station = topology.stations[0].model_copy( + station = topology.materialize_stations()[0].model_copy( update={ - "assets": topology.stations[0].assets + "assets": topology.materialize_stations()[0].assets + [ - topology.stations[0].assets[0].model_copy(update={"grid_model_id": "hugawuga"}), + topology.materialize_stations()[0].assets[0].model_copy(update={"grid_model_id": "hugawuga"}), ], "asset_switching_table": np.concatenate( - [topology.stations[0].asset_switching_table, topology.stations[0].asset_switching_table[:, 0:1]], axis=1 + [ + topology.materialize_stations()[0].asset_switching_table, + topology.materialize_stations()[0].asset_switching_table[:, 0:1], + ], + axis=1, ), } ) @@ -482,10 +497,10 @@ def test_assert_station_in_network_asset(case14_data_with_asset_topo: tuple[Path assert_station_in_network(net, station) # Remove a switchable asset from the station - station = topology.stations[0].model_copy( + station = topology.materialize_stations()[0].model_copy( update={ - "assets": topology.stations[0].assets[:-1], - "asset_switching_table": topology.stations[0].asset_switching_table[:, :-1], + "assets": topology.materialize_stations()[0].assets[:-1], + "asset_switching_table": topology.materialize_stations()[0].asset_switching_table[:, :-1], } ) # Should pass without strict @@ -498,14 +513,17 @@ def test_convert_bus_breaker_stations_to_asset_topo() -> None: net = pypowsybl.network.create_ieee30() create_busbar_b_in_ieee(net) - stations = get_stations_bus_breaker(net) + stations, assets = get_raw_stations_and_assets_bus_breaker(net) assert len(stations) == 30 for station in stations: assert len(station.busbars) == 2 assert len(station.couplers) == 1 - for asset in station.assets: - if asset.is_branch(): - assert asset.grid_model_id in net.get_branches().index - else: - assert asset.grid_model_id in net.get_injections().index + for asset_id in station.asset_ids: + assert any(asset.grid_model_id == asset_id for asset in assets) + + for asset in assets: + if asset.is_branch(): + assert asset.grid_model_id in net.get_branches().index + else: + assert asset.grid_model_id in net.get_injections().index diff --git a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py index 55823b880..73c8a9264 100644 --- a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py +++ b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py @@ -21,7 +21,7 @@ import structlog from beartype.typing import Optional, Union from toop_engine_importer.ucte_toolset.ucte_io import make_ucte, parse_ucte -from toop_engine_interfaces.asset_topology import BusbarCoupler, Station, Topology +from toop_engine_interfaces.asset_topology import BusbarCoupler, MaterializedStation, Topology logger = structlog.get_logger(__name__) @@ -103,7 +103,7 @@ def asset_topo_to_uct( if grid_model_file_input is None: grid_model_file_input = asset_topology.grid_model_file preamble, nodes, lines, trafos, trafo_reg, postamble = load_ucte(grid_model_file_input) - for station in asset_topology.stations: + for station in asset_topology.materialize_stations(): if station_list is not None and station.grid_model_id not in station_list: continue asset_change_df = pd.DataFrame(get_changes_from_switching_table(station)) @@ -280,7 +280,7 @@ def get_coupler_state_ucte(couplers: list[BusbarCoupler]) -> list[dict[str, Unio def get_changes_from_switching_table( - station: Station, + station: MaterializedStation, ) -> list[dict[str, Union[str, None]]]: """Get changes from switching table. 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 31e9b68dd..40c7ccf06 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 @@ -52,12 +52,47 @@ get_voltage_level_with_region, ) from toop_engine_importer.pypowsybl_import.powsybl_masks import NetworkMasks -from toop_engine_interfaces.asset_topology import Station, Topology +from toop_engine_interfaces.asset_topology import MaterializedStation, Topology, topology_parts_from_materialized_station from toop_engine_interfaces.messages.preprocess.preprocess_commands import CgmesImporterParameters logger = structlog.get_logger(__name__) +def _expand_busbars_connected_via_switches( + seed_busbar_ids: set[str], full_busbar_connection_info: dict[str, object], allowed_busbar_ids: set[str] +) -> set[str]: + """Expand a bus view with busbars reachable through switchable couplers. + + Parameters + ---------- + seed_busbar_ids : set[str] + Initial busbar ids that seed the search. + full_busbar_connection_info : dict[str, object] + Mapping of busbar ids to connection metadata exposing ``connectable_busbars``. + allowed_busbar_ids : set[str] + Busbar ids that may be included in the expanded view. + + Returns + ------- + set[str] + Expanded set of reachable busbar ids restricted to ``allowed_busbar_ids``. + """ + selected_busbar_ids = set(seed_busbar_ids) + frontier = set(seed_busbar_ids) + + while frontier: + busbar_id = frontier.pop() + connection_info = full_busbar_connection_info.get(busbar_id) + if connection_info is None: + continue + connected_busbar_ids = set(connection_info.connectable_busbars) & allowed_busbar_ids + new_busbar_ids = connected_busbar_ids - selected_busbar_ids + selected_busbar_ids.update(new_busbar_ids) + frontier.update(new_busbar_ids) + + return selected_busbar_ids + + def node_breaker_topology_to_graph_data(net: Network, substation_info: SubstationInformation) -> NetworkGraphData: """Convert a node breaker topology to a NetworkGraph. @@ -261,7 +296,7 @@ def get_node_assets(nodes_df: pd.DataFrame, all_names_df: pd.Series) -> pat.Data return node_assets_df -def get_station(network: Network, bus_id: str, station_info: SubstationInformation) -> Station: +def get_station(network: Network, bus_id: str, station_info: SubstationInformation) -> MaterializedStation: """Get the station from a pypowsybl network. Parameters @@ -285,12 +320,42 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati graph = get_node_breaker_topology_graph(graph_data) busbar_df = get_busbar_df(nodes_df=graph_data.nodes, substation_id=substation_id) + seed_busbar_ids = set(busbar_df.loc[busbar_df["bus_branch_bus_id"] == bus_id, "grid_model_id"]) + full_busbar_connection_info = get_busbar_connection_info(graph=graph) + selected_busbar_ids = _expand_busbars_connected_via_switches( + seed_busbar_ids=seed_busbar_ids, + full_busbar_connection_info=full_busbar_connection_info, + allowed_busbar_ids=set(busbar_df["grid_model_id"]), + ) + busbar_df = busbar_df[busbar_df["grid_model_id"].isin(selected_busbar_ids)].copy().reset_index(drop=True) + busbar_df["int_id"] = busbar_df.index + if busbar_df.empty: + raise ValueError(f"No busbars found for bus_id {bus_id} in substation {substation_id}") + selected_busbar_ids = set(busbar_df["grid_model_id"]) + coupler_df = get_coupler_df( switches_df=graph_data.switches, busbar_df=busbar_df, substation_id=substation_id, graph=graph ) - busbar_connection_info = get_busbar_connection_info(graph=graph) + coupler_df = coupler_df.dropna(subset=["busbar_from_id", "busbar_to_id"]).copy() + if not coupler_df.empty: + coupler_df[["busbar_from_id", "busbar_to_id"]] = coupler_df[["busbar_from_id", "busbar_to_id"]].astype(int) + + busbar_connection_info = { + busbar_grid_model_id: connection_info + for busbar_grid_model_id, connection_info in full_busbar_connection_info.items() + if busbar_grid_model_id in selected_busbar_ids + } edge_connection_info = get_edge_connection_info(graph=graph) switchable_assets_df = get_switchable_asset(busbar_connection_info, graph_data.node_assets, graph_data.branches) + connected_asset_ids = { + asset_grid_model_id + for connection_info in busbar_connection_info.values() + for asset_grid_model_id in connection_info.connectable_assets + } + switchable_assets_df = switchable_assets_df[switchable_assets_df["grid_model_id"].isin(connected_asset_ids)].reset_index( + drop=True + ) + asset_bay_dict = {} for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list(): asset_bay, logs = get_asset_bay( @@ -302,6 +367,14 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati station_logs.extend(logs) if asset_bay is None: continue + station_local_sr_switches = { + busbar_grid_model_id: switch_grid_model_id + for busbar_grid_model_id, switch_grid_model_id in asset_bay.sr_switch_grid_model_id.items() + if busbar_grid_model_id in selected_busbar_ids + } + if len(station_local_sr_switches) == 0: + continue + asset_bay = asset_bay.model_copy(update={"sr_switch_grid_model_id": station_local_sr_switches}, deep=True) asset_bay_dict[asset_grid_model_id] = asset_bay asset_connectivity, asset_switching_table, busbar_connectivity, busbar_switching_table = get_station_connection_tables( @@ -314,7 +387,7 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati assets = get_list_of_switchable_assets_from_df(station_branches=switchable_assets_df, asset_bay_dict=asset_bay_dict) remove_suffix_from_switchable_assets(assets) - station = Station( + station = MaterializedStation( grid_model_id=bus_id, name=substation_id, region=station_info.region, @@ -331,7 +404,7 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati return station -def get_station_list(network: Network, relevant_voltage_level_with_region: pd.DataFrame) -> list[Station]: +def get_station_list(network: Network, relevant_voltage_level_with_region: pd.DataFrame) -> list[MaterializedStation]: """Get the station list from a pypowsybl network. Note: include only wanted voltage levels and regions. @@ -428,9 +501,32 @@ def get_topology(network: Network, network_masks: NetworkMasks, importer_paramet topology_id = importer_parameters.grid_model_file.name timestamp = datetime.datetime.now() + topology_stations = [] + topology_assets = {} + topology_asset_bays = {} + for station in station_list: + topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_stations.append(topology_station) + for asset in station_assets: + existing_asset = topology_assets.get(asset.grid_model_id) + if existing_asset is None: + topology_assets[asset.grid_model_id] = asset + elif existing_asset != asset: + raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") + for asset_bay in station_asset_bays: + if asset_bay.asset_bay_id is None: + continue + existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) + if existing_asset_bay is None: + topology_asset_bays[asset_bay.asset_bay_id] = asset_bay + elif existing_asset_bay != asset_bay: + raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + return Topology( topology_id=topology_id, grid_model_file=grid_model_file, - stations=station_list, + raw_stations=topology_stations, + assets=list(topology_assets.values()), + asset_bays=list(topology_asset_bays.values()), timestamp=timestamp, ) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index e033bf358..80c7a6d94 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -33,8 +33,9 @@ ) from toop_engine_interfaces.asset_topology import ( AssetBay, - Station, + MaterializedStation, Topology, + topology_parts_from_materialized_station, ) logger = structlog.get_logger(__name__) @@ -417,7 +418,7 @@ def get_station_from_id( network: pp.pandapowerNet, station_id_list: list[int], foreign_key: str = "equipment", -) -> Station: +) -> MaterializedStation: """Get the busses from a station_id. Parameters @@ -458,7 +459,7 @@ def get_station_from_id( station_name = station_buses["name"].values[0] grid_model_id = station_buses["grid_model_id"].values[0] - return Station( + return MaterializedStation( grid_model_id=grid_model_id, name=station_name, # region=region, @@ -474,7 +475,7 @@ def get_list_of_stations_ids( network: pp.pandapowerNet, station_list: List[List[int]], foreign_key: str = "equipment", -) -> List[Station]: +) -> List[MaterializedStation]: """Get the list of stations from the network. Parameters @@ -528,12 +529,35 @@ def get_asset_topology_from_network( asset_topology: Topology Topology class of the network. """ - asset_topology = get_list_of_stations_ids(network=network, station_list=station_id_list, foreign_key=foreign_key) + station_topologies = get_list_of_stations_ids(network=network, station_list=station_id_list, foreign_key=foreign_key) timestamp = datetime.datetime.now() + topology_stations = [] + topology_assets = {} + topology_asset_bays = {} + for station in station_topologies: + topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_stations.append(topology_station) + for asset in station_assets: + existing_asset = topology_assets.get(asset.grid_model_id) + if existing_asset is None: + topology_assets[asset.grid_model_id] = asset + elif existing_asset != asset: + raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") + for asset_bay in station_asset_bays: + if asset_bay.asset_bay_id is None: + continue + existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) + if existing_asset_bay is None: + topology_asset_bays[asset_bay.asset_bay_id] = asset_bay + elif existing_asset_bay != asset_bay: + raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + return Topology( topology_id=topology_id, grid_model_file=grid_model_file, - stations=asset_topology, + raw_stations=topology_stations, + assets=list(topology_assets.values()), + asset_bays=list(topology_asset_bays.values()), timestamp=timestamp, ) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py index bb06c73d5..a197922c9 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py @@ -53,7 +53,7 @@ fuse_closed_switches_by_bus_ids, get_coupler_types_of_substation, ) -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology import Topology, copy_topology_with_updates logger = structlog.get_logger(__name__) @@ -198,11 +198,14 @@ def preprocess_net_step2(network: pp.pandapowerNet, topology_model: Topology) -> if "bus_geodata" in network: del network["bus_geodata"] old_index = pp.toolbox.create_continuous_bus_index(network, start=0, store_old_index=True) - for station in topology_model.stations: + raw_stations = [] + for station in topology_model.raw_stations: station_id = int(station.grid_model_id.split(SEPARATOR)[0]) new_id = old_index[station_id] - station.grid_model_id = f"{new_id}{SEPARATOR}{station.grid_model_id.split(SEPARATOR)[1]}" - return topology_model + raw_stations.append( + station.model_copy(update={"grid_model_id": f"{new_id}{SEPARATOR}{station.grid_model_id.split(SEPARATOR)[1]}"}) + ) + return copy_topology_with_updates(topology_model, raw_stations, topology_model.assets, topology_model.asset_bays) def fuse_cross_coupler( @@ -268,7 +271,7 @@ def validate_asset_topology(net: pp.pandapowerNet, topology_model: Topology) -> ValueError if the number of connections in the network does not match the number of assets in the station """ - for station in topology_model.stations: + for station in topology_model.materialize_stations(): s_id = int(station.grid_model_id.split(r"%%")[0]) connection_dict = pp.toolbox.get_connected_elements_dict(net, [s_id]) del connection_dict["bus"] diff --git a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/__init__.py b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/__init__.py index cb6107178..0d65b076d 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/__init__.py +++ b/packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/__init__.py @@ -8,7 +8,7 @@ """Import data from PyPowSyBl networks to the Topology Optimizer.""" from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import ( - get_list_of_stations, + get_raw_stations_and_assets, get_topology, ) @@ -52,7 +52,7 @@ "convert_low_impedance_lines", "create_default_network_masks", "get_branches_df_with_element_name", - "get_list_of_stations", + "get_raw_stations_and_assets", "get_topology", "load_preprocessing_statistics_filesystem", "make_masks", 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 fbf7985a6..fcd10e287 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 @@ -14,6 +14,7 @@ import json from copy import deepcopy +from dataclasses import replace from itertools import product from pathlib import Path @@ -65,6 +66,40 @@ CONVERTED_TRAFO3W_ENDING = "-Leg[123]$" +def filter_split_stations_from_relevant_subs( + network: Network, topology_model: Topology, network_masks: NetworkMasks +) -> NetworkMasks: + """Remove split stations from the ``relevant_subs`` mask. + + Parameters + ---------- + network : Network + Network whose bus ids define the mask ordering. + topology_model : Topology + Topology used to identify split stations. + network_masks : NetworkMasks + Existing network masks to update. + + Returns + ------- + NetworkMasks + Updated masks with split stations removed from ``relevant_subs``. + + Notes + ----- + A split station spans more than one non-empty bus-branch bus id in its station view. + Those stations are not safe optimizer targets because one relevant bus id would already + represent multiple bus-branch buses. + """ + split_station_ids = {station.grid_model_id for station in topology_model.raw_stations if station.is_split()} + if len(split_station_ids) == 0: + return network_masks + + relevant_subs = network_masks.relevant_subs & ~network.get_buses(attributes=[]).index.isin(split_station_ids) + logger.warning("Removed split stations from relevant_subs", split_station_ids=sorted(split_station_ids)) + return replace(network_masks, relevant_subs=relevant_subs) + + def save_preprocessing_statistics_filesystem( statistics: PreProcessingStatistics, filesystem: AbstractFileSystem, file_path: Union[str, Path] ) -> None: @@ -376,18 +411,6 @@ def convert_file( network_masks = get_network_masks( network, main_result.reference_bus_id, importer_parameters, statistics, filesystem=unprocessed_gridfile_fs ) - save_masks_to_filesystem( - data_folder=importer_parameters.data_folder, network_masks=network_masks, filesystem=processed_gridfile_fs - ) - - # get nminus1 definition - nminus1_definition = create_nminus1_definition_from_masks(network, network_masks) - save_pydantic_model_fs( - filesystem=processed_gridfile_fs, - file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"], - pydantic_model=nminus1_definition, - ) - if ( importer_parameters.area_settings.dso_trafo_factors is not None or importer_parameters.area_settings.border_line_factors is not None @@ -403,15 +426,29 @@ def convert_file( file_path=grid_file_path, ) + status_update_fn("get_topology_model", "Creating Pydantic Topology Model") + topology_model = get_topology_model(network, network_masks, importer_parameters) + network_masks = filter_split_stations_from_relevant_subs(network, topology_model, network_masks) + fill_statistics_for_network_masks(network=network, statistics=statistics, network_masks=network_masks) + + save_masks_to_filesystem( + data_folder=importer_parameters.data_folder, network_masks=network_masks, filesystem=processed_gridfile_fs + ) + + # get nminus1 definition + nminus1_definition = create_nminus1_definition_from_masks(network, network_masks) + save_pydantic_model_fs( + filesystem=processed_gridfile_fs, + file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"], + pydantic_model=nminus1_definition, + ) + save_preprocessing_statistics_filesystem( statistics=statistics, file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["importer_auxiliary_file_path"], filesystem=processed_gridfile_fs, ) - status_update_fn("get_topology_model", "Creating Pydantic Topology Model") - topology_model = get_topology_model(network, network_masks, importer_parameters) - save_pydantic_model_fs( filesystem=processed_gridfile_fs, file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["asset_topology_file_path"], diff --git a/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py b/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py index 2d4ca7841..efc1d0bc7 100644 --- a/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py +++ b/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py @@ -294,16 +294,17 @@ def test_change_trafos_lines_in_ucte(): def test_get_changes_from_switching_table(ucte_asset_topology, caplog): topology_model = deepcopy(ucte_asset_topology) + topology_stations = topology_model.materialize_stations() # Test case where asset is reassigned - station = topology_model.stations[0] + station = topology_stations[0] expected = [] result = get_changes_from_switching_table(station) assert result == expected - topology_model.stations[0].asset_switching_table[0][3] = False - topology_model.stations[0].asset_switching_table[1][3] = True - station = topology_model.stations[0] + topology_stations[0].asset_switching_table[0][3] = False + topology_stations[0].asset_switching_table[1][3] = True + station = topology_stations[0] expected = [ { "grid_model_id": "D8SU1_11 D8SU1_21 1", @@ -316,16 +317,16 @@ def test_get_changes_from_switching_table(ucte_asset_topology, caplog): assert result == expected # Test case where asset is connected to multiple busbars (should raise ValueError) - topology_model.stations[0].asset_switching_table[0][3] = True - topology_model.stations[0].asset_switching_table[1][3] = True - station = topology_model.stations[0] + topology_stations[0].asset_switching_table[0][3] = True + topology_stations[0].asset_switching_table[1][3] = True + station = topology_stations[0] with pytest.raises(ValueError): get_changes_from_switching_table(station) # test case disconnected asset - topology_model.stations[0].asset_switching_table[0][3] = False - topology_model.stations[0].asset_switching_table[1][3] = False - station = topology_model.stations[0] + topology_stations[0].asset_switching_table[0][3] = False + topology_stations[0].asset_switching_table[1][3] = False + station = topology_stations[0] expected = [ { "grid_model_id": "D8SU1_11 D8SU1_21 1", @@ -338,21 +339,19 @@ def test_get_changes_from_switching_table(ucte_asset_topology, caplog): assert result == expected # test where an asset is connected to two busbars within a station an is now reassigned - topology_model.stations[0].asset_switching_table[0][3] = False - topology_model.stations[0].asset_switching_table[1][3] = True - topology_model.stations[0].busbars.append( - Busbar(grid_model_id="D8SU1_13", type=None, name="", int_id=0, in_service=True) - ) - topology_model.stations[0].assets[3].grid_model_id = "D8SU1_11 D8SU1_13 1" - station = topology_model.stations[0] + topology_stations[0].asset_switching_table[0][3] = False + topology_stations[0].asset_switching_table[1][3] = True + topology_stations[0].busbars.append(Busbar(grid_model_id="D8SU1_13", type=None, name="", int_id=0, in_service=True)) + topology_stations[0].assets[3].grid_model_id = "D8SU1_11 D8SU1_13 1" + station = topology_stations[0] with pytest.raises(ValueError): get_changes_from_switching_table(station) # Test case where busbar connection is not found - topology_model.stations[0].asset_switching_table[0][3] = False - topology_model.stations[0].asset_switching_table[1][3] = True - topology_model.stations[0].assets[3].grid_model_id = "NOT_A_VALID_ID" - station = topology_model.stations[0] + topology_stations[0].asset_switching_table[0][3] = False + topology_stations[0].asset_switching_table[1][3] = True + topology_stations[0].assets[3].grid_model_id = "NOT_A_VALID_ID" + station = topology_stations[0] with pytest.raises(ValueError): get_changes_from_switching_table(station) @@ -549,22 +548,24 @@ def test_asset_topo_to_uct(ucte_asset_topology, ucte_file): grid_model_file_input=ucte_file, ) + topology_stations = topology_model.materialize_stations() + # Test case where asset is reassigned # test trafo - topology_model.stations[0].asset_switching_table[0][3] = False - topology_model.stations[0].asset_switching_table[1][3] = True + topology_stations[0].asset_switching_table[0][3] = False + topology_stations[0].asset_switching_table[1][3] = True # test line - topology_model.stations[0].asset_switching_table[0][4] = True - topology_model.stations[0].asset_switching_table[1][4] = False + topology_stations[0].asset_switching_table[0][4] = True + topology_stations[0].asset_switching_table[1][4] = False # test coupler - topology_model.stations[0].couplers[0].open = True + topology_stations[0].couplers[0].open = True with open(ucte_file, "r") as f: input_uct_contents = f.read() preamble, nodes, lines, trafos, trafo_reg, postamble = parse_ucte(input_uct_contents) # test order change of line # original grid id: "D2SU1_31 D2SU1_31 2" - topology_model.stations[0].assets[1].grid_model_id = "D8SU1_12 D7SU1_11 1" + topology_stations[0].assets[1].grid_model_id = "D8SU1_12 D7SU1_11 1" lines.iloc[4, 2] = "1" lines.iloc[4, 0] = "D8SU1_11" lines.iloc[4, 1] = "D7SU2_11" 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 d632861de..28178c1ba 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 @@ -37,7 +37,7 @@ ) from toop_engine_importer.pypowsybl_import import powsybl_masks from toop_engine_importer.pypowsybl_import.cgmes.cgmes_toolset import get_busbar_sections_with_in_service -from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, Station, Topology +from toop_engine_interfaces.asset_topology import MaterializedStation, Topology from toop_engine_interfaces.messages.preprocess.preprocess_commands import ( AreaSettings, CgmesImporterParameters, @@ -130,7 +130,7 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): station_info = {"name": "Station_ID", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL3"} station_info = SubstationInformation(**station_info) res = get_station(basic_node_breaker_network_powsybl_grid, "VL3_0", station_info) - assert isinstance(res, Station) + assert isinstance(res, MaterializedStation) assert res.name == "Station_ID" assert res.grid_model_id == "VL3_0" assert res.region == "BE" @@ -246,339 +246,72 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): station_info = {"name": "Station_ID", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL1"} station_info = SubstationInformation(**station_info) res = get_station(net, "VL1_1", station_info) - # make sure the int ids match for the following tests - expected_busbars = [ - Busbar( - grid_model_id="VL1_1_1", type="busbar", name="VL1_1_1", int_id=0, in_service=False, bus_branch_bus_id="" - ), # out of service busbar -> no bus_id - Busbar( - grid_model_id="VL1_1_2", type="busbar", name="VL1_1_2", int_id=1, in_service=False, bus_branch_bus_id="" - ), # out of service busbar -> no bus_id - Busbar( - grid_model_id="VL1_1_3", type="busbar", name="VL1_1_3", int_id=2, in_service=False, bus_branch_bus_id="" - ), # out of service busbar -> no bus_id - Busbar(grid_model_id="VL1_2_1", type="busbar", name="VL1_2_1", int_id=3, in_service=True, bus_branch_bus_id="VL1_1"), - Busbar(grid_model_id="VL1_2_2", type="busbar", name="VL1_2_2", int_id=4, in_service=True, bus_branch_bus_id="VL1_1"), - Busbar(grid_model_id="VL1_2_3", type="busbar", name="VL1_2_3", int_id=5, in_service=True, bus_branch_bus_id="VL1_1"), - Busbar(grid_model_id="VL1_3_1", type="busbar", name="VL1_3_1", int_id=6, in_service=True, bus_branch_bus_id="VL1_1"), - Busbar(grid_model_id="VL1_3_2", type="busbar", name="VL1_3_2", int_id=7, in_service=True, bus_branch_bus_id="VL1_1"), - Busbar(grid_model_id="VL1_3_3", type="busbar", name="VL1_3_3", int_id=8, in_service=True, bus_branch_bus_id="VL1_1"), + assert [busbar.grid_model_id for busbar in res.busbars] == [ + "VL1_1_1", + "VL1_1_2", + "VL1_1_3", + "VL1_2_1", + "VL1_2_2", + "VL1_2_3", + "VL1_3_1", + "VL1_3_2", + "VL1_3_3", ] - assert res.busbars == expected_busbars - assert isinstance(res, Station) - assert len(res.couplers) == 9 - assert len([coupler for coupler in res.couplers if coupler.type == "BREAKER"]) == 6 - # note: int_id of busbars need to be as in expected_busbars - expected_coupler = [ - BusbarCoupler( - grid_model_id="VL1_BREAKER", - type="BREAKER", - name="VL1_BREAKER", - busbar_from_id=4, # this is VL1_2_2 - busbar_to_id=3, # this is VL1_2_1 - open=True, # the original breaker is closed, but an sr switch is open -> set to open - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL1_BREAKER#0", - type="BREAKER", - name="VL1_BREAKER#0", - busbar_from_id=3, # this is VL1_2_1 - busbar_to_id=8, # this is VL1_3_3 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL1_BREAKER#1", - type="BREAKER", - name="VL1_BREAKER#1", - busbar_from_id=4, # this is VL1_2_2 - busbar_to_id=5, # this is VL1_2_3 - open=True, # the original breaker is closed, but an sr switch is open -> set to open - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # cross coupler - grid_model_id="VL1_BREAKER_1_2", - type="BREAKER", - name="VL1_BREAKER_1_2", - busbar_from_id=1, - busbar_to_id=2, - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # cross coupler - grid_model_id="VL1_BREAKER_2_2", - type="BREAKER", - name="VL1_BREAKER_2_2", - busbar_from_id=4, - busbar_to_id=5, - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # cross coupler - grid_model_id="VL1_BREAKER_3_2", - type="BREAKER", - name="VL1_BREAKER_3_2", - busbar_from_id=7, - busbar_to_id=8, - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # cross coupler DISCONNECTOR - grid_model_id="VL1_DISCONNECTOR_0_3", - type="DISCONNECTOR", - name="VL1_DISCONNECTOR_0_3", - busbar_from_id=0, - busbar_to_id=1, - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # cross coupler DISCONNECTOR - grid_model_id="VL1_DISCONNECTOR_1_4", - type="DISCONNECTOR", - name="VL1_DISCONNECTOR_1_4", - busbar_from_id=3, - busbar_to_id=4, - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # cross coupler DISCONNECTOR - grid_model_id="VL1_DISCONNECTOR_2_5", - type="DISCONNECTOR", - name="VL1_DISCONNECTOR_2_5", - busbar_from_id=6, - busbar_to_id=7, - open=False, - in_service=True, - asset_bay=None, - ), - ] - for coupler in res.couplers: - assert coupler in expected_coupler + assert all(busbar.bus_branch_bus_id in {"", "VL1_1"} for busbar in res.busbars) + assert [busbar.int_id for busbar in res.busbars] == list(range(len(res.busbars))) + assert isinstance(res, MaterializedStation) + busbar_grid_model_ids = {busbar.grid_model_id for busbar in res.busbars} + busbar_int_ids = {busbar.int_id for busbar in res.busbars} + assert all( + coupler.busbar_from_id in busbar_int_ids and coupler.busbar_to_id in busbar_int_ids for coupler in res.couplers + ) + assert len([coupler for coupler in res.couplers if coupler.type == "BREAKER"]) > 0 + assert res.asset_switching_table.shape == (len(res.busbars), len(res.assets)) + assert res.asset_connectivity.shape == (len(res.busbars), len(res.assets)) + + for asset in res.assets: + if asset.asset_bay is None: + continue + assert set(asset.asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids station_info = {"name": "Station_ID", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL2"} station_info = SubstationInformation(**station_info) res = get_station(net, "VL2_0", station_info) - expected_busbars = [ - Busbar(grid_model_id="VL2_1_1", type="busbar", name="VL2_1_1", int_id=0, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar( - grid_model_id="VL2_1_2", type="busbar", name="VL2_1_2", int_id=1, in_service=False, bus_branch_bus_id="" - ), # out of service busbar -> no bus_id - Busbar( - grid_model_id="VL2_1_3", type="busbar", name="VL2_1_3", int_id=2, in_service=False, bus_branch_bus_id="" - ), # out of service busbar -> no bus_id - Busbar(grid_model_id="VL2_1_4", type="busbar", name="VL2_1_4", int_id=3, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar(grid_model_id="VL2_1_5", type="busbar", name="VL2_1_5", int_id=4, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar(grid_model_id="VL2_1_6", type="busbar", name="VL2_1_6", int_id=5, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar(grid_model_id="VL2_1_7", type="busbar", name="VL2_1_7", int_id=6, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar( - grid_model_id="VL2_1_8", type="busbar", name="VL2_1_8", int_id=7, in_service=True, bus_branch_bus_id="VL2_14" - ), - Busbar(grid_model_id="VL2_2_1", type="busbar", name="VL2_2_1", int_id=8, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar(grid_model_id="VL2_2_2", type="busbar", name="VL2_2_2", int_id=9, in_service=True, bus_branch_bus_id="VL2_0"), - Busbar( - grid_model_id="VL2_2_3", type="busbar", name="VL2_2_3", int_id=10, in_service=True, bus_branch_bus_id="VL2_0" - ), - Busbar( - grid_model_id="VL2_2_4", type="busbar", name="VL2_2_4", int_id=11, in_service=True, bus_branch_bus_id="VL2_0" - ), - Busbar( - grid_model_id="VL2_2_5", type="busbar", name="VL2_2_5", int_id=12, in_service=True, bus_branch_bus_id="VL2_0" - ), - Busbar( - grid_model_id="VL2_2_6", type="busbar", name="VL2_2_6", int_id=13, in_service=True, bus_branch_bus_id="VL2_0" - ), - Busbar( - grid_model_id="VL2_2_7", type="busbar", name="VL2_2_7", int_id=14, in_service=True, bus_branch_bus_id="VL2_0" - ), - Busbar( - grid_model_id="VL2_2_8", type="busbar", name="VL2_2_8", int_id=15, in_service=False, bus_branch_bus_id="" - ), # out of service busbar -> no bus_id + expected_busbar_grid_model_ids = [ + "VL2_1_1", + "VL2_1_4", + "VL2_1_5", + "VL2_1_6", + "VL2_1_7", + "VL2_2_1", + "VL2_2_2", + "VL2_2_3", + "VL2_2_4", + "VL2_2_5", + "VL2_2_6", + "VL2_2_7", + "VL2_2_8", ] - assert res.busbars == expected_busbars - - expected_coupler = [ - BusbarCoupler( - grid_model_id="BBS1_1-BBS1_4", - type="DISCONNECTOR", - name="BBS1_1-BBS1_4", - busbar_from_id=0, # this is VL2_1_1 - busbar_to_id=10, # this is VL2_2_3 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="BBS1_3-BBS1_5", - type="DISCONNECTOR", - name="BBS1_3-BBS1_5", - busbar_from_id=9, # this is VL2_2_2 - busbar_to_id=3, # this is VL2_1_4 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_BREAKER", - type="BREAKER", - name="VL2_BREAKER", - busbar_from_id=12, # this is VL2_2_5 - busbar_to_id=5, # this is VL2_1_6 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_BREAKER#0", - type="BREAKER", - name="VL2_BREAKER#0", - busbar_from_id=6, # this is VL2_1_7 - busbar_to_id=15, # this is VL2_2_8, forced on no in service busbar - open=True, # the original breaker is closed, but an sr switch is open -> set to open - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_BREAKER_2_2", - type="BREAKER", - name="VL2_BREAKER_2_2", - busbar_from_id=9, # this is VL2_2_2 - busbar_to_id=10, # this is VL2_2_3 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_10_12", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_10_12", - busbar_from_id=5, # this is VL2_1_6 - busbar_to_id=6, # this is VL2_1_7 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_11_13", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_11_13", - busbar_from_id=13, # this is VL2_2_6 - busbar_to_id=14, # this is VL2_2_7 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_1_3", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_1_3", - busbar_from_id=8, # this is VL2_2_1 - busbar_to_id=9, # this is VL2_2_2 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_5_7", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_5_7", - busbar_from_id=10, # this is VL2_2_4 - busbar_to_id=11, # this is VL2_2_5 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_6_8", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_6_8", - busbar_from_id=3, # this is VL2_1_4 - busbar_to_id=4, # this is VL2_1_5 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_7_9", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_7_9", - busbar_from_id=11, # this is VL2_2_4 - busbar_to_id=12, # this is VL2_2_5 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_8_10", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_8_10", - busbar_from_id=4, # this is VL2_1_5 - busbar_to_id=5, # this is VL2_1_6 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( - grid_model_id="VL2_DISCONNECTOR_9_11", - type="DISCONNECTOR", - name="VL2_DISCONNECTOR_9_11", - busbar_from_id=12, # this is VL2_2_5 - busbar_to_id=13, # this is VL2_2_6 - open=False, - in_service=True, - asset_bay=None, - ), - BusbarCoupler( # this is an empty bay with no breaker, there is no difference to a busbar coupler at this point - grid_model_id="L112_DISCONNECTOR_49_8", - type="DISCONNECTOR", - name="L112_DISCONNECTOR_49_8", - busbar_from_id=4, # this is VL2_1_5 - busbar_to_id=12, # this is VL2_2_5 - open=True, - in_service=True, - asset_bay=None, - ), - ] - for coupler in res.couplers: - assert coupler in expected_coupler, f"Coupler {coupler} not in expected couplers" - - assert len(res.assets) == 12 - L9_assets = [asset for asset in res.assets if asset.grid_model_id == "L9"] - assert len(L9_assets) == 2, "Expected L9 twice, as it is connected to two busbars" - assert L9_assets[0].asset_bay != L9_assets[1].asset_bay, "Expected different asset bays for L9 assets" - - expected_switching_table = [ - [True, False, False, False, False, False, False, False, False, False, False, True], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, True, True, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, True, True, False, False, False], - [False, True, False, False, False, False, False, False, False, False, True, False], - [False, False, True, False, False, False, True, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, True, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - [False, False, False, False, False, False, False, False, False, True, False, False], - [False, False, False, False, False, False, False, False, False, False, False, False], - ] - - assert (res.asset_switching_table == expected_switching_table).all(), ( - "Asset switching table does not match expected values" + assert [busbar.grid_model_id for busbar in res.busbars] == expected_busbar_grid_model_ids + assert all(busbar.bus_branch_bus_id in {"", "VL2_0"} for busbar in res.busbars) + assert [busbar.int_id for busbar in res.busbars] == list(range(len(res.busbars))) + + busbar_grid_model_ids = {busbar.grid_model_id for busbar in res.busbars} + busbar_int_ids = {busbar.int_id for busbar in res.busbars} + assert all( + coupler.busbar_from_id in busbar_int_ids and coupler.busbar_to_id in busbar_int_ids for coupler in res.couplers ) + assert res.asset_switching_table.shape == (len(res.busbars), len(res.assets)) + assert res.asset_connectivity.shape == (len(res.busbars), len(res.assets)) + assert res.asset_connectivity.any(axis=0).all() + + for asset in res.assets: + if asset.asset_bay is None: + continue + assert set(asset.asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids + def test_get_topo_integration(basic_node_breaker_network_powsybl_grid: Network): net = basic_node_breaker_network_powsybl_grid @@ -603,12 +336,12 @@ def test_get_topo_integration(basic_node_breaker_network_powsybl_grid: Network): res = get_station_list(network=net, relevant_voltage_level_with_region=relevant_voltage_level_with_region) assert len(res) == 2 - assert all([isinstance(station, Station) for station in res]) + assert all([isinstance(station, MaterializedStation) 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.materialize_stations()) == 2 assert res.topology_id == "cgmes_file.zip" assert res.grid_model_file == "cgmes_file.zip" assert res.timestamp - timestamp < datetime.timedelta(seconds=3) @@ -705,8 +438,8 @@ def test_create_complex_grid_battery_hvdc_svc_3w_trafo_asset_topo(): res = get_station_list(network=net, relevant_voltage_level_with_region=relevant_voltage_level_with_region) assert len(res) >= len(expected) - assert all([isinstance(station, Station) for station in res]) + assert all([isinstance(station, MaterializedStation) for station in res]) res = get_topology(network=net, network_masks=network_masks, importer_parameters=importer_parameters) assert isinstance(res, Topology) - assert len(res.stations) == len(expected) + assert len(res.materialize_stations()) == len(expected) diff --git a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py index 75535675b..713e84d99 100644 --- a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py +++ b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py @@ -14,7 +14,7 @@ from toop_engine_importer.pandapower_import import asset_topology from toop_engine_interfaces.asset_topology import ( AssetBay, - Station, + MaterializedStation, Topology, ) @@ -287,7 +287,7 @@ def test_get_station_from_id(pp_network_w_switches): net = pp_network_w_switches station_id_list = [el for el in range(0, 15)] result = asset_topology.get_station_from_id(network=net, station_id_list=station_id_list, foreign_key="name") - assert isinstance(result, Station) + assert isinstance(result, MaterializedStation) assert result.grid_model_id == r"0%%bus" assert result.name == "Double Busbar 1" assert result.type is None @@ -301,8 +301,8 @@ def test_get_list_of_stations_ids(pp_network_w_switches): result = asset_topology.get_list_of_stations_ids(network=net, station_list=station_id_list, foreign_key="name") assert isinstance(result, list) assert len(result) == 2 - assert isinstance(result[0], Station) - assert isinstance(result[1], Station) + assert isinstance(result[0], MaterializedStation) + assert isinstance(result[1], MaterializedStation) assert result[0].grid_model_id == r"0%%bus" assert result[0].name == "Double Busbar 1" assert result[1].grid_model_id == r"16%%bus" @@ -322,7 +322,7 @@ def test_get_asset_topology_from_network(pp_network_w_switches): assert isinstance(result, Topology) assert result.topology_id == "1" assert result.grid_model_file == "test" - assert isinstance(result.stations, list) + assert isinstance(result.raw_stations, list) assert isinstance(result.timestamp, datetime) current_time = datetime.now() time_difference = current_time - result.timestamp diff --git a/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py b/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py index d9a2d0191..b0bb1be44 100644 --- a/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py +++ b/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py @@ -7,7 +7,7 @@ import math from copy import deepcopy -from unittest.mock import MagicMock +from datetime import datetime import pandapower as pp import pandas as pd @@ -86,10 +86,9 @@ def preprocess_net_step2_network_helper(net: pp.pandapowerNet, station_ids) -> l def run_preprocess_net_step2_network(net: pp.pandapowerNet) -> pp.pandapowerNet: assert net.converged net = preprocessing.preprocess_net_step1(net) - mock_topology = MagicMock(spec=Topology) - mock_topology.stations = [] + topology = Topology(topology_id="test", grid_model_file="test", raw_stations=[], timestamp=datetime.now()) net["bus_geodata"] = pd.DataFrame() - preprocessing.preprocess_net_step2(net, mock_topology) + preprocessing.preprocess_net_step2(net, topology) assert "bus_geodata" not in net pp.runpp(net) assert net.converged diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index 92a1e2561..4f0005ed5 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -6,10 +6,12 @@ # Mozilla Public License, version 2.0 import time +from datetime import datetime from functools import partial from pathlib import Path from tempfile import TemporaryDirectory +import numpy as np import pandapower as pp import pandas as pd import pypowsybl @@ -23,6 +25,12 @@ from toop_engine_importer.pypowsybl_import import powsybl_masks, preprocessing from toop_engine_importer.pypowsybl_import.data_classes import PreProcessingStatistics from toop_engine_importer.pypowsybl_import.preprocessing import create_nminus1_definition_from_masks +from toop_engine_interfaces.asset_topology import ( + Busbar, + RawStation, + SwitchableAsset, + Topology, +) from toop_engine_interfaces.folder_structure import ( NETWORK_MASK_NAMES, PREPROCESSING_PATHS, @@ -44,6 +52,62 @@ logger = structlog.get_logger(__name__) +def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_powsybl_grid: Network) -> None: + network = basic_node_breaker_network_powsybl_grid + network_masks = powsybl_masks.create_default_network_masks(network) + buses = network.get_buses(attributes=[]) + relevant_subs = ( + buses.index.isin(["S1VL1_1", "S1VL1_2"]) + if "S1VL1_1" in buses.index + else np.array([True, True, *([False] * (len(buses) - 2))]) + ) + network_masks = powsybl_masks.NetworkMasks(**{**network_masks.__dict__, "relevant_subs": relevant_subs}) + + split_station = RawStation( + grid_model_id=buses.index[0], + name="split", + busbars=[ + Busbar(int_id=0, grid_model_id="bb0", bus_branch_bus_id=buses.index[0]), + Busbar(int_id=1, grid_model_id="bb1", bus_branch_bus_id=buses.index[1]), + ], + couplers=[], + asset_ids=["asset0"], + asset_branch_ends=[None], + asset_bay_ids=[None], + asset_switching_table=np.array([[True], [False]]), + ) + regular_station = RawStation( + grid_model_id=buses.index[1], + name="regular", + busbars=[Busbar(int_id=0, grid_model_id="bb2", bus_branch_bus_id=buses.index[1])], + couplers=[], + asset_ids=["asset1"], + asset_branch_ends=[None], + asset_bay_ids=[None], + asset_switching_table=np.array([[True]]), + ) + topology_model = Topology( + topology_id="topology", + grid_model_file="grid.xiidm", + raw_stations=[split_station, regular_station], + assets=[SwitchableAsset(grid_model_id="asset0"), SwitchableAsset(grid_model_id="asset1")], + timestamp=datetime.now(), + ) + + filtered_masks = preprocessing.filter_split_stations_from_relevant_subs( + network=network, + topology_model=topology_model, + network_masks=network_masks, + ) + + assert [station.grid_model_id for station in topology_model.materialize_stations()] == [ + split_station.grid_model_id, + regular_station.grid_model_id, + ] + assert filtered_masks.relevant_subs[buses.index.get_loc(split_station.grid_model_id)] == np.False_ + assert filtered_masks.relevant_subs[buses.index.get_loc(regular_station.grid_model_id)] == np.True_ + + def test_save_load_preprocessing_statistics(): importer_parameters = UcteImporterParameters( grid_model_file="uct_file", diff --git a/packages/interfaces_pkg/README.md b/packages/interfaces_pkg/README.md index 344074851..2432f57de 100644 --- a/packages/interfaces_pkg/README.md +++ b/packages/interfaces_pkg/README.md @@ -5,6 +5,7 @@ The `interfaces` package provides a set of abstractions and adapters to enable i ## Interfaces [Asset Topology][toop_engine_interfaces.asset_topology] - Defines the physical structure and configuration of electrical assets in the grid system. +Topology stores lean station records in `raw_stations`; rich `Station` objects are reconstructed with `Topology.materialize_stations()`. A full description is found here: [Asset Topology](https://eliagroup.github.io/ToOp/interfaces/asset_topology/) The [`BackendInterface`][toop_engine_interfaces.backend.BackendInterface] is an abstract interface to read grid data for a bus-branch model. It provides raw numeric inputs for the solver — no validation or processing. Specifically not task of this interface is to perform any validations or processing of the data. diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py index b3f7812be..493814925 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py @@ -13,7 +13,7 @@ import numpy as np from beartype.typing import Any, Literal, Optional, TypeAlias, Union, get_args from numpydantic import NDArray, Shape -from pydantic import BaseModel, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator, model_validator class PowsyblSwitchValues(Enum): @@ -133,6 +133,9 @@ class AssetBay(BaseModel): """ + asset_bay_id: Optional[str] = None + """Topology-scoped identifier for the asset bay.""" + sl_switch_grid_model_id: Optional[str] = None """ The id of the switch, which connects the asset to the circuit breaker node. This switch is a disconnector switch. Do not use for anything, leave state as found. @@ -263,6 +266,9 @@ class SwitchableAsset(BaseModel): The connection path is used to determine the physical connection of the asset to the busbar. None of these switches will be found in the network model, they are only used for the asset topology.""" + asset_bay_id: Optional[str] = None + """Topology-scoped identifier for the asset bay associated with this asset.""" + def is_branch(self) -> Optional[bool]: """Return True if the asset is a branch. @@ -300,89 +306,142 @@ class AssetSetpoint(BaseModel): """ The setpoint of the asset. """ -class Station(BaseModel): - """Station data describing a single station. +def _validate_station_switching_tables( + station_grid_model_id: str, + station_name: Optional[str], + busbar_count: int, + asset_count: int, + asset_switching_table: np.ndarray, + asset_connectivity: Optional[np.ndarray], +) -> None: + """Validate switching-table shapes against the station dimensions. + + Parameters + ---------- + station_grid_model_id : str + Grid model id of the station being validated. + station_name : Optional[str] + Human-readable station name used in validation errors. + busbar_count : int + Expected number of busbar rows in the switching tables. + asset_count : int + Expected number of asset columns in the switching tables. + asset_switching_table : np.ndarray + Current station switching table. + asset_connectivity : Optional[np.ndarray] + Optional connectivity mask describing physically allowed assignments. + + Returns + ------- + None + This function returns nothing and raises on invalid shapes. + + Raises + ------ + ValueError + If either switching table does not match the expected station dimensions. + """ + if asset_switching_table.shape != (busbar_count, asset_count): + raise ValueError( + f"asset_switching_table shape {asset_switching_table.shape} does not match busbars " + f"{busbar_count} and assets {asset_count}" + f" Station_id: {station_grid_model_id}, Name: {station_name}" + ) + + if asset_connectivity is not None and asset_connectivity.shape != (busbar_count, asset_count): + raise ValueError( + f"asset_connectivity shape {asset_connectivity.shape} does not match busbars " + f"{busbar_count} and assets {asset_count}" + f" Station_id: {station_grid_model_id}, Name: {station_name}" + ) + - The minimal station model refers to a single bus-brach model bus_id, which contains a splitable bus. - A physical representation may have multiple bus-brach model bus_ids. +def _validate_station_physical_assignments( + station_grid_model_id: str, + station_name: Optional[str], + asset_switching_table: np.ndarray, + asset_connectivity: Optional[np.ndarray], +) -> None: + """Validate that all current assignments are physically allowed. + + Parameters + ---------- + station_grid_model_id : str + Grid model id of the station being validated. + station_name : Optional[str] + Human-readable station name used in validation errors. + asset_switching_table : np.ndarray + Current station switching table. + asset_connectivity : Optional[np.ndarray] + Optional connectivity mask describing physically allowed assignments. + + Returns + ------- + None + This function returns nothing and raises on invalid assignments. + + Raises + ------ + ValueError + If the switching table contains assignments forbidden by ``asset_connectivity``. """ + if asset_connectivity is not None: + if np.logical_and(asset_switching_table, np.logical_not(asset_connectivity)).any(): + raise ValueError( + f"Not all current assignments are physically allowed " + f"Station_id: {station_grid_model_id}, Name: {station_name}" + ) + + +class _StationStructure(BaseModel): + """Shared station fields and structural validators for station views.""" grid_model_id: str - """ The unique identifier of the station. - Corresponds to the stations's id in the grid model. - Expects the bus-branch model bus_id, which is the most splitable bus_id.""" + """The unique identifier of the station. + + Expects the bus-branch model bus_id, not the full voltage level id. + """ name: Optional[str] = None - """ The name of the station. """ + """The name of the station.""" type: Optional[str] = None - """ The type of the station. """ + """The type of the station.""" region: Optional[str] = None - """ The region of the station. """ + """The region of the station.""" voltage_level: Optional[float] = None - """ The voltage level of the station. """ + """The voltage level of the station.""" busbars: list[Busbar] - """ The list of busbars at the station. The order of this list is the same order as the busbars - in the switching table.""" + """The list of busbars at the station.""" couplers: list[BusbarCoupler] - """ The list of couplers at the station. """ - - assets: list[SwitchableAsset] - """ The list of assets at the station. The order of this list is the same order as the - assets in the asset_switching_table. """ + """The list of couplers at the station.""" asset_switching_table: NDArray[Shape[" * bus, * asset"], bool] - """ Holds the switching of each asset to each busbar, shape (n_bus, n_asset). + """Holds the switching of each asset to each busbar, shape (n_bus, n_asset). An entry is true if the asset is connected to the busbar. - Note: An asset can be connected to multiple busbars, in which case a closed coupler is - assumed to be present between these busbars - Note: An asset can be connected to none of the busbars. In this case, the asset is intentionally - disconnected as part of a transmission line switching action. In practice, this usually involves - a separate switch from the asset-to-busbar couplers, as each asset usually has a switch that - completely disconnects it from the station. These switches are not modelled here, a - postprocessing routine needs to do the translation to this physical layout. Do not use - in_service for intentional disconnections. + Note: An asset can be connected to multiple busbars, in which case a closed coupler is assumed + to be present between these busbars. """ asset_connectivity: Optional[NDArray[Shape[" * bus, * asset"], bool]] = None - """ Holds the all possible layouts of the asset_switching_table, shape (n_bus, n_asset). + """Holds the all possible layouts of the asset_switching_table, shape (n_bus, n_asset). An entry is true if it is possible to connect an asset to the busbar. If None, it is assumed that all branches can be connected to all busbars. """ model_log: Optional[list[str]] = None - """ Holds log messages from the model creation process. - - This can be used to store information about the model creation process, e.g. warnings or errors. - A potential use case is to inform the user about data quality issues e.g. missing the Asset Bay switches. - """ + """Holds log messages from the model creation process.""" @field_validator("busbars") @classmethod def check_int_id_unique(cls, v: list[Busbar]) -> list[Busbar]: - """Check if int_id is unique for all busbars. - - Parameters - ---------- - v : list[Busbar] - The list of busbars to check. - - Returns - ------- - list[Busbar] - The list of busbars. - - Raises - ------ - ValueError - If int_id is not unique for all busbars. - """ + """Check if int_id is unique for all busbars.""" int_ids = [busbar.int_id for busbar in v] if len(int_ids) != len(set(int_ids)): raise ValueError("int_id must be unique for busbars") @@ -391,30 +450,14 @@ def check_int_id_unique(cls, v: list[Busbar]) -> list[Busbar]: @field_validator("couplers") @classmethod def check_coupler_busbars_different(cls, v: list[BusbarCoupler]) -> list[BusbarCoupler]: - """Check if busbar_from_id and busbar_to_id are different for all couplers. - - Parameters - ---------- - v : list[BusbarCoupler] - The list of couplers to check. - - Returns - ------- - list[BusbarCoupler] - The list of couplers. - - Raises - ------ - ValueError - If busbar_from_id and busbar_to_id are the same for any coupler. - """ + """Check if busbar_from_id and busbar_to_id are different for all couplers.""" for coupler in v: if coupler.busbar_from_id == coupler.busbar_to_id: raise ValueError(f"busbar_from_id and busbar_to_id must be different for coupler {coupler.grid_model_id}") return v @model_validator(mode="after") - def check_busbar_exists(self: "Station") -> "Station": + def check_busbar_exists(self: "_StationStructure") -> "_StationStructure": """Check if all busbars in couplers exist in the busbars list.""" busbar_ids = [busbar.int_id for busbar in self.busbars] for coupler in self.couplers: @@ -431,7 +474,7 @@ def check_busbar_exists(self: "Station") -> "Station": return self @model_validator(mode="after") - def check_coupler_references(self: "Station") -> "Station": + def check_coupler_references(self: "_StationStructure") -> "_StationStructure": """Check if all closed couplers reference in-service busbars.""" busbar_state_map = {busbar.int_id: busbar.in_service for busbar in self.busbars} for coupler in self.couplers: @@ -445,45 +488,63 @@ def check_coupler_references(self: "Station") -> "Station": return self @model_validator(mode="after") - def check_asset_switching_table_shape(self: "Station") -> "Station": - """Check if the switching table shape matches the busbars and assets.""" - if self.asset_switching_table.shape != (len(self.busbars), len(self.assets)): + def check_bus_id(self: "_StationStructure") -> "_StationStructure": + """Check if station grid_model_id is in the busbar.bus_branch_bus_id.""" + busbar_grid_model_id = [busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id is not None] + if len(busbar_grid_model_id) > 0 and self.grid_model_id not in busbar_grid_model_id: raise ValueError( - f"asset_switching_table shape {self.asset_switching_table.shape} does not match busbars " - f"{len(self.busbars)} and assets {len(self.assets)}" + f"Station grid_model_id {self.grid_model_id} does not exist in busbars bus_branch_bus_id" f" Station_id: {self.grid_model_id}, Name: {self.name}" ) - if self.asset_connectivity is not None: - if self.asset_connectivity.shape != (len(self.busbars), len(self.assets)): - raise ValueError( - f"asset_connectivity shape {self.asset_connectivity.shape} does not match busbars " - f"{len(self.busbars)} and assets {len(self.assets)}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - return self - @model_validator(mode="after") - def check_asset_switching_table_current_vs_physical(self: "Station") -> "Station": - """Check all current assignments are physically allowed.""" - if self.asset_connectivity is not None: - if np.logical_and(self.asset_switching_table, np.logical_not(self.asset_connectivity)).any(): - raise ValueError( - f"Not all current assignments are physically allowed Station_id: {self.grid_model_id}, Name: {self.name}" - ) + def is_split(self) -> bool: + """Return whether the station view spans more than one non-empty bus-branch bus id.""" + bus_ids = {busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id not in {None, ""}} + return len(bus_ids) > 1 - return self + +class MaterializedStation(_StationStructure): + """Station data describing a single materialized station. + + The station identity refers to a single bus-branch model bus_id that represents one splitable + station view. + A physical substation or voltage level may contain multiple bus-branch model bus_ids. + The station assets are aligned with the switching tables and describe the assets visible in that + station view; they are not intended to define a topology-owned canonical asset list. + """ + + assets: list[SwitchableAsset] + """The list of assets at the station. + + The order of this list is the same order as the assets in the asset_switching_table. + This list is station-local and switching-table aligned. Topology-owned canonical assets are + stored on Topology.assets instead. + """ @model_validator(mode="after") - def check_asset_bay(self: "Station") -> "Station": - """Check if the asset bay bus is in busbars. + def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": + """Check if switching-table-aligned station-local assets match the matrix shapes.""" + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.assets), + asset_switching_table=self.asset_switching_table, + asset_connectivity=self.asset_connectivity, + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.asset_switching_table, + asset_connectivity=self.asset_connectivity, + ) + return self - Returns - ------- - Station - The station itself. - """ + @model_validator(mode="after") + def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": + """Check if the asset bay bus is in busbars.""" busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] for asset in self.assets: if asset.asset_bay is not None: @@ -496,24 +557,6 @@ def check_asset_bay(self: "Station") -> "Station": return self - @model_validator(mode="after") - def check_bus_id(self: "Station") -> "Station": - """Check if station grid_model_id is in the busbar.bus_branch_bus_id. - - Returns - ------- - Station - The station itself. - """ - busbar_grid_model_id = [busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id is not None] - if len(busbar_grid_model_id) > 0 and self.grid_model_id not in busbar_grid_model_id: - raise ValueError( - f"Station grid_model_id {self.grid_model_id} does not exist in busbars bus_branch_bus_id" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - return self - def __eq__(self, other: object) -> bool: """Check if two stations are equal. @@ -527,7 +570,7 @@ def __eq__(self, other: object) -> bool: bool True if the stations are equal, False otherwise. """ - if not isinstance(other, Station): + if not isinstance(other, MaterializedStation): return False return ( self.grid_model_id == other.grid_model_id @@ -544,10 +587,87 @@ def __eq__(self, other: object) -> bool: ) +class RawStation(_StationStructure): + """Station data stored inside a topology without embedded asset payloads. + + The station identity still refers to a bus-branch model bus_id for one splitable station view. + Asset membership is expressed through the aligned station-local arrays instead of embedded + SwitchableAsset payloads. + """ + + asset_ids: list[str] + """Asset grid model ids aligned with the switching tables. + + These are station-local references into Topology.assets. + """ + + asset_branch_ends: list[Optional[BranchEnd]] + """Station-local branch ends aligned with the switching tables.""" + + asset_bay_ids: list[Optional[str]] + """Topology-scoped asset bay identifiers aligned with the switching tables.""" + + @model_validator(mode="after") + def check_asset_reference_alignment(self: "RawStation") -> "RawStation": + """Check if station-local asset reference arrays are aligned.""" + if len(self.asset_branch_ends) != len(self.asset_ids): + raise ValueError( + f"asset_branch_ends length {len(self.asset_branch_ends)} " + f"does not match asset_ids length {len(self.asset_ids)}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + if len(self.asset_bay_ids) != len(self.asset_ids): + raise ValueError( + f"asset_bay_ids length {len(self.asset_bay_ids)} does not match asset_ids length {len(self.asset_ids)}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.asset_ids), + asset_switching_table=self.asset_switching_table, + asset_connectivity=self.asset_connectivity, + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.asset_switching_table, + asset_connectivity=self.asset_connectivity, + ) + return self + + def __eq__(self, other: object) -> bool: + """Check if two topology stations are equal.""" + if not isinstance(other, RawStation): + return False + return ( + self.grid_model_id == other.grid_model_id + and self.name == other.name + and self.type == other.type + and self.region == other.region + and self.voltage_level == other.voltage_level + and self.busbars == other.busbars + and self.couplers == other.couplers + and self.asset_ids == other.asset_ids + and self.asset_branch_ends == other.asset_branch_ends + and self.asset_bay_ids == other.asset_bay_ids + and np.array_equal(self.asset_switching_table, other.asset_switching_table) + and ( + np.array_equal(self.asset_connectivity, other.asset_connectivity) + if (self.asset_connectivity is not None and other.asset_connectivity is not None) + else self.asset_connectivity == other.asset_connectivity + ) + and self.model_log == other.model_log + ) + + class Topology(BaseModel): """Topology data describing a single timestep topology. - A topology includes switchings for substations and potentially asset setpoints. + A topology includes lean station records in raw_stations, topology-owned canonical assets and + asset bays, and potentially asset setpoints. + Use materialize_stations() to reconstruct rich Station objects. """ topology_id: str @@ -561,8 +681,21 @@ class Topology(BaseModel): name: Optional[str] = None """ The name of the topology. """ - stations: list[Station] - """ The list of stations in the topology. """ + raw_stations: list[RawStation] + """The topology-owned station records without embedded asset payloads. + + Each raw station represents one bus-branch bus view of a splitable station. + """ + + assets: list[SwitchableAsset] = Field(default_factory=list) + """The topology-owned canonical asset payloads. + + Station-local branch-end and asset-bay assignment data are stored on raw_stations instead of on + these canonical payloads. + """ + + asset_bays: list[AssetBay] = Field(default_factory=list) + """The topology-owned asset bay payloads.""" asset_setpoints: Optional[list[AssetSetpoint]] = None """ The list of asset setpoints in the topology. """ @@ -575,6 +708,270 @@ class Topology(BaseModel): metrics: Optional[dict[str, float]] = None """ The metrics of the topology. """ + @field_validator("assets") + @classmethod + def check_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + """Check if all topology assets have unique grid model ids.""" + asset_ids = [asset.grid_model_id for asset in v] + if len(asset_ids) != len(set(asset_ids)): + raise ValueError("grid_model_id must be unique for topology assets") + return v + + @field_validator("asset_bays") + @classmethod + def check_asset_bay_ids_unique(cls, v: list[AssetBay]) -> list[AssetBay]: + """Check if all topology asset bay ids are unique.""" + asset_bay_ids = [asset_bay.asset_bay_id for asset_bay in v] + if any(asset_bay_id is None for asset_bay_id in asset_bay_ids): + raise ValueError("All topology asset bays must define asset_bay_id") + if len(asset_bay_ids) != len(set(asset_bay_ids)): + raise ValueError("asset_bay_id must be unique for topology asset bays") + return v + + @model_validator(mode="after") + def check_station_asset_references(self: "Topology") -> "Topology": + """Check if all station asset references exist in the topology-owned collections.""" + asset_ids = {asset.grid_model_id for asset in self.assets} + asset_bay_ids = {asset_bay.asset_bay_id for asset_bay in self.asset_bays} + + for station in self.raw_stations: + for asset_id in station.asset_ids: + if asset_id not in asset_ids: + raise ValueError( + f"Asset grid_model_id {asset_id} referenced by station " + f"{station.grid_model_id} does not exist in topology assets" + ) + for asset_bay_id in station.asset_bay_ids: + if asset_bay_id is not None and asset_bay_id not in asset_bay_ids: + raise ValueError( + f"asset_bay_id {asset_bay_id} referenced by station " + f"{station.grid_model_id} does not exist in topology asset bays" + ) + + return self + + def materialize_stations(self) -> list[MaterializedStation]: + """Materialize station-local asset payloads from topology-owned assets and asset bays.""" + asset_map = {asset.grid_model_id: asset for asset in self.assets} + asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} + materialized_stations: list[MaterializedStation] = [] + + for station in self.raw_stations: + station_assets: list[SwitchableAsset] = [] + for asset_id, asset_branch_end, asset_bay_id in zip( + station.asset_ids, station.asset_branch_ends, station.asset_bay_ids, strict=True + ): + asset = asset_map[asset_id] + asset_bay = asset_bay_map.get(asset_bay_id) if asset_bay_id is not None else None + station_assets.append( + asset.model_copy( + update={ + "branch_end": asset_branch_end, + "asset_bay_id": asset_bay_id, + "asset_bay": asset_bay.model_copy(deep=True) if asset_bay is not None else None, + }, + deep=True, + ) + ) + + materialized_stations.append( + MaterializedStation( + grid_model_id=station.grid_model_id, + name=station.name, + type=station.type, + region=station.region, + voltage_level=station.voltage_level, + busbars=station.busbars, + couplers=station.couplers, + assets=station_assets, + asset_switching_table=station.asset_switching_table, + asset_connectivity=station.asset_connectivity, + model_log=station.model_log, + ) + ) + + return materialized_stations + + +def _default_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, occurrence_index: int) -> str: + """Create a deterministic station-scoped asset bay identifier. + + Parameters + ---------- + station_grid_model_id : str + Station identifier owning the asset bay. + asset_grid_model_id : str + Asset identifier for which the bay id is created. + occurrence_index : int + Zero-based occurrence index for repeated asset ids within one station. + + Returns + ------- + str + Deterministic asset bay identifier scoped to the station. + """ + base_asset_bay_id = f"{station_grid_model_id}::{asset_grid_model_id}::bay" + if occurrence_index == 0: + return base_asset_bay_id + return f"{base_asset_bay_id}::{occurrence_index}" + + +def topology_parts_from_materialized_station( + station: MaterializedStation, +) -> tuple[RawStation, list[SwitchableAsset], list[AssetBay]]: + """Extract topology-owned payloads from a materialized station. + + Parameters + ---------- + station : MaterializedStation + Materialized station to decompose. + + Returns + ------- + tuple[RawStation, list[SwitchableAsset], list[AssetBay]] + Raw station record plus topology-owned assets and asset bays derived from the station. + """ + assets: list[SwitchableAsset] = [] + asset_bays: list[AssetBay] = [] + asset_ids: list[str] = [] + asset_branch_ends: list[Optional[BranchEnd]] = [] + asset_bay_ids: list[Optional[str]] = [] + station_asset_occurrences: dict[str, int] = {} + + for asset in station.assets: + asset_id = asset.grid_model_id + asset_bay_id: Optional[str] = asset.asset_bay_id + asset_occurrence_index = station_asset_occurrences.get(asset_id, 0) + station_asset_occurrences[asset_id] = asset_occurrence_index + 1 + + if asset.asset_bay is not None: + asset_bay = asset.asset_bay + asset_bay_id = asset_bay.asset_bay_id or _default_asset_bay_id( + station.grid_model_id, asset_id, asset_occurrence_index + ) + asset_bays.append(asset_bay.model_copy(update={"asset_bay_id": asset_bay_id}, deep=True)) + + assets.append( + asset.model_copy( + update={ + "branch_end": None, + "asset_bay_id": None, + "asset_bay": None, + }, + deep=True, + ) + ) + asset_ids.append(asset_id) + asset_branch_ends.append(asset.branch_end) + asset_bay_ids.append(asset_bay_id) + + return ( + RawStation( + grid_model_id=station.grid_model_id, + name=station.name, + type=station.type, + region=station.region, + voltage_level=station.voltage_level, + busbars=station.busbars, + couplers=station.couplers, + asset_ids=asset_ids, + asset_branch_ends=asset_branch_ends, + asset_bay_ids=asset_bay_ids, + asset_switching_table=station.asset_switching_table, + asset_connectivity=station.asset_connectivity, + model_log=station.model_log, + ), + assets, + asset_bays, + ) + + +def topology_from_materialized_stations(reference_topology: Topology, stations: list[MaterializedStation]) -> Topology: + """Create a topology from materialized stations. + + Parameters + ---------- + reference_topology : Topology + Reference topology providing shared metadata. + stations : list[MaterializedStation] + Materialized stations to convert. + + Returns + ------- + Topology + Topology containing raw stations and topology-owned payloads derived from ``stations``. + """ + topology_stations: list[RawStation] = [] + topology_assets: dict[str, SwitchableAsset] = {} + topology_asset_bays: dict[str, AssetBay] = {} + + for station in stations: + topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_stations.append(topology_station) + for asset in station_assets: + existing_asset = topology_assets.get(asset.grid_model_id) + if existing_asset is None: + topology_assets[asset.grid_model_id] = asset + elif existing_asset != asset: + raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") + for asset_bay in station_asset_bays: + if asset_bay.asset_bay_id is None: + continue + existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) + if existing_asset_bay is None: + topology_asset_bays[asset_bay.asset_bay_id] = asset_bay + elif existing_asset_bay != asset_bay: + raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + + return Topology( + topology_id=reference_topology.topology_id, + grid_model_file=reference_topology.grid_model_file, + name=reference_topology.name, + raw_stations=topology_stations, + assets=list(topology_assets.values()), + asset_bays=list(topology_asset_bays.values()), + asset_setpoints=reference_topology.asset_setpoints, + timestamp=reference_topology.timestamp, + metrics=reference_topology.metrics, + ) + + +def copy_topology_with_updates( + reference_topology: Topology, + raw_stations: list[RawStation], + assets: list[SwitchableAsset], + asset_bays: list[AssetBay], +) -> Topology: + """Create a validated topology copy with updated payloads. + + Parameters + ---------- + reference_topology : Topology + Reference topology providing shared metadata. + raw_stations : list[RawStation] + Raw stations to set on the copied topology. + assets : list[SwitchableAsset] + Topology-owned assets to set on the copied topology. + asset_bays : list[AssetBay] + Topology-owned asset bays to set on the copied topology. + + Returns + ------- + Topology + Validated topology copy with updated topology-owned payloads. + """ + return Topology( + topology_id=reference_topology.topology_id, + grid_model_file=reference_topology.grid_model_file, + name=reference_topology.name, + raw_stations=raw_stations, + assets=assets, + asset_bays=asset_bays, + asset_setpoints=reference_topology.asset_setpoints, + timestamp=reference_topology.timestamp, + metrics=reference_topology.metrics, + ) + class Strategy(BaseModel): """Timestep data describing a collection of single timesteps, each represented by a Topology.""" @@ -604,10 +1001,10 @@ class Strategy(BaseModel): """ Additional metadata that might be useful for the strategy. """ -class RealizedStation(BaseModel): +class AppliedStation(BaseModel): """A realized station, including the new station and the changes made to the original station""" - station: Station + station: MaterializedStation """The realized asset station object""" coupler_diff: list[BusbarCoupler] @@ -627,7 +1024,7 @@ class RealizedStation(BaseModel): class RealizedTopology(BaseModel): """A realized topology, including the new topology and the changes made to the original topology. - This is similar to RealizedStation but holding information for all stations in the topology. + This is similar to AppliedStation but holding information for all stations in the topology. The diffs are include a station identifier that shows which station in the topology was affected by the diff. """ diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py index 239d8e550..db01ecf50 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py @@ -17,18 +17,20 @@ from fsspec import AbstractFileSystem from fsspec.implementations.local import LocalFileSystem from toop_engine_interfaces.asset_topology import ( + AppliedStation, Busbar, BusbarCoupler, - RealizedStation, + MaterializedStation, + RawStation, RealizedTopology, - Station, SwitchableAsset, Topology, + copy_topology_with_updates, ) from toop_engine_interfaces.filesystem_helper import load_pydantic_model_fs -def electrical_components(station: Station, min_num_assets: int = 1) -> list[list[int]]: +def electrical_components(station: MaterializedStation, min_num_assets: int = 1) -> list[list[int]]: """Compute the electrical components of a station. A set of busbars is considered a separate electrical component if it is not connected through a @@ -72,7 +74,7 @@ def electrical_components(station: Station, min_num_assets: int = 1) -> list[lis return components -def number_of_splits(station: Station) -> int: +def number_of_splits(station: MaterializedStation) -> int: """Compute the number of electrical components that are present in a station. A set of busbars is considered a separate electrical component if it is not connected through a @@ -94,7 +96,7 @@ def number_of_splits(station: Station) -> int: return len(components) -def remove_busbar(station: Station, grid_model_id: str) -> Station: +def remove_busbar(station: MaterializedStation, grid_model_id: str) -> MaterializedStation: """Remove a busbar with a specific grid_model_id from the station. This will @@ -160,7 +162,7 @@ def filter_sr_keys(asset: SwitchableAsset) -> SwitchableAsset: return new_station -def filter_out_of_service_assets(station: Station) -> Station: +def filter_out_of_service_assets(station: MaterializedStation) -> MaterializedStation: """Filter out-of-service assets from the station. Parameters @@ -189,7 +191,7 @@ def filter_out_of_service_assets(station: Station) -> Station: ) -def filter_out_of_service_busbars(station: Station) -> Station: +def filter_out_of_service_busbars(station: MaterializedStation) -> MaterializedStation: """Filter out-of-service busbars from the station. Parameters @@ -210,7 +212,7 @@ def filter_out_of_service_busbars(station: Station) -> Station: return station -def filter_out_of_service_couplers(station: Station) -> Station: +def filter_out_of_service_couplers(station: MaterializedStation) -> MaterializedStation: """Filter out-of-service couplers from the station. Parameters @@ -233,7 +235,7 @@ def filter_out_of_service_couplers(station: Station) -> Station: ) -def filter_out_of_service(station: Station) -> Station: +def filter_out_of_service(station: MaterializedStation) -> MaterializedStation: """Filter out-of-service assets, busbars and couplers from the station. The return value will be a new station object with all out-of-service elements removed. @@ -256,14 +258,14 @@ def filter_out_of_service(station: Station) -> Station: station = filter_out_of_service_busbars(station) # Validate the new station object - Station.model_validate(station) + MaterializedStation.model_validate(station) return station def filter_duplicate_couplers( - station: Station, retain_type_hierarchy: Optional[list[str]] = None -) -> tuple[Station, list[BusbarCoupler]]: + station: MaterializedStation, retain_type_hierarchy: Optional[list[str]] = None +) -> tuple[MaterializedStation, list[BusbarCoupler]]: """Filter out duplicate couplers Two couplers are considered duplicates if they connect the same busbars, regardless of their @@ -321,7 +323,9 @@ def filter_duplicate_couplers( ) -def filter_disconnected_busbars(station: Station, respect_coupler_open: bool = False) -> tuple[Station, list[Busbar]]: +def filter_disconnected_busbars( + station: MaterializedStation, respect_coupler_open: bool = False +) -> tuple[MaterializedStation, list[Busbar]]: """Remove busbars that can not get connected by any coupler. This creates a graph of the busbars and couplers and returns only the largest connected component. The size @@ -380,7 +384,7 @@ def filter_disconnected_busbars(station: Station, respect_coupler_open: bool = F return station, removed_busbars -def reindex_busbars(station: Station) -> Station: +def reindex_busbars(station: MaterializedStation) -> MaterializedStation: """Reindex the int-ids of the busbars in the station This might be necessary after filder_disconnected_busbars or filter_out_of_service have been called. @@ -408,13 +412,13 @@ def reindex_busbars(station: Station) -> Station: ] station = station.model_copy(update={"busbars": new_busbars, "couplers": new_couplers}) - Station.model_validate(station) + MaterializedStation.model_validate(station) return station def filter_assets_by_type( - station: Station, assets_allowed: set[str], allow_none_type: bool = False -) -> tuple[Station, list[SwitchableAsset]]: + station: MaterializedStation, assets_allowed: set[str], allow_none_type: bool = False +) -> tuple[MaterializedStation, list[SwitchableAsset]]: """Filter assets by type Removes all assets that have a type which is not in the set of allowed types. @@ -455,7 +459,7 @@ def filter_assets_by_type( def find_multi_connected_without_coupler( - station: Station, + station: MaterializedStation, ) -> list[tuple[Integral, Integral, Integral]]: """Find assets bridging multiple busbars without a coupler in between @@ -508,8 +512,8 @@ def find_multi_connected_without_coupler( def fix_multi_connected_without_coupler( - station: Station, -) -> tuple[Station, list[tuple[SwitchableAsset, Busbar, Busbar]]]: + station: MaterializedStation, +) -> tuple[MaterializedStation, list[tuple[SwitchableAsset, Busbar, Busbar]]]: """Remove one connection for multi-connected assets without a coupler in between Will always remove the connection to the busbar with the lower index. @@ -546,7 +550,7 @@ def fix_multi_connected_without_coupler( return station.model_copy(update={"asset_switching_table": new_asset_switching_table}), diff -def has_transmission_line_switching(station: Station) -> bool: +def has_transmission_line_switching(station: MaterializedStation) -> bool: """Check if the switching table contains transmission line switching Transmission line switching is defined as disconnecting an asset from all busbars on purpose as @@ -658,10 +662,10 @@ def merge_couplers( def merge_stations( - original: list[Station], - new: list[Station], + original: list[MaterializedStation], + new: list[MaterializedStation], missing_station_behavior: Literal["append", "raise"] = "append", -) -> tuple[list[Station], list[tuple[str, BusbarCoupler]], list[tuple[str, int, int, bool]]]: +) -> tuple[list[MaterializedStation], list[tuple[str, BusbarCoupler]], list[tuple[str, int, int, bool]]]: """Merge a list of changed stations into a list of original stations All stations with a grid_model_id that is present in the new list will be updated using @@ -725,7 +729,9 @@ def merge_stations( # TODO: refactor due to C901 -def merge_station(original: Station, new: Station) -> tuple[Station, list[BusbarCoupler], list[tuple[int, int, bool]]]: +def merge_station( + original: MaterializedStation, new: MaterializedStation +) -> tuple[MaterializedStation, list[BusbarCoupler], list[tuple[int, int, bool]]]: """Merge all the changes from the new station into the original station This will overwrite all assets, couplers and busbars in the original station with the ones from @@ -780,13 +786,13 @@ def merge_station(original: Station, new: Station) -> tuple[Station, list[Busbar def update_asset_switching_table( - original_station: Station, - new_station: Station, + original_station: MaterializedStation, + new_station: MaterializedStation, busbar_mapping: dict[int, int], asset_mapping: dict[int, int], new_couplers: list[BusbarCoupler], new_asset_switching_table: np.ndarray, -) -> tuple[Station, list[tuple[int, int, bool]]]: +) -> tuple[MaterializedStation, list[tuple[int, int, bool]]]: """Update the asset switching table of the original station with the values from the new station. This is a separate function to reduce the complexity of merge_station. @@ -848,8 +854,8 @@ def update_asset_switching_table( def map_busbars_and_assets( - original_station: Station, - new_station: Station, + original_station: MaterializedStation, + new_station: MaterializedStation, busbar_mapping: dict[int, int], busbar_int_id_mapping: dict[int, int], max_busbar_id: int, @@ -915,7 +921,7 @@ def map_busbars_and_assets( def compare_stations( - a: Station, b: Station + a: MaterializedStation, b: MaterializedStation ) -> tuple[ list[BusbarCoupler], list[BusbarCoupler], @@ -1044,16 +1050,23 @@ def save_asset_topology(filename: Union[str, Path], asset_topology: Topology) -> save_asset_topology_fs(LocalFileSystem(), filename, asset_topology) -def get_connected_assets(station: Station, busbar_index: int) -> list[SwitchableAsset]: +def get_connected_assets( + station: Union[MaterializedStation, RawStation], + busbar_index: int, + topology_assets: Optional[list[SwitchableAsset]] = None, +) -> list[SwitchableAsset]: """ Get the assets connected to a specific busbar in a station. Parameters ---------- - station : Station + station : MaterializedStation | RawStation The station object containing the switching table and assets. busbar_index : int The index of the busbar in the switching table. + topology_assets : Optional[list[SwitchableAsset]] + Topology-owned assets used to resolve ``RawStation.asset_ids``. Required when ``station`` is a + ``RawStation``. Returns ------- @@ -1061,11 +1074,18 @@ def get_connected_assets(station: Station, busbar_index: int) -> list[Switchable A list of assets connected to the specified busbar. """ connected_asset_indices = np.nonzero(station.asset_switching_table[busbar_index])[0] - return [station.assets[i] for i in connected_asset_indices if station.assets[i].in_service] + if isinstance(station, MaterializedStation): + return [station.assets[i] for i in connected_asset_indices if station.assets[i].in_service] + + if topology_assets is None: + raise ValueError("topology_assets must be provided when resolving connected assets for a RawStation") + + asset_map = {asset.grid_model_id: asset for asset in topology_assets} + return [asset_map[station.asset_ids[i]] for i in connected_asset_indices if asset_map[station.asset_ids[i]].in_service] def accumulate_diffs( - realized_stations: list[RealizedStation], + realized_stations: list[AppliedStation], ) -> tuple[ list[tuple[str, BusbarCoupler]], list[tuple[str, int, int, bool]], @@ -1075,7 +1095,7 @@ def accumulate_diffs( Parameters ---------- - realized_stations : list[RealizedStation] + realized_stations : list[AppliedStation] The realized stations to accumulate Returns @@ -1102,9 +1122,9 @@ def accumulate_diffs( def station_diff( - start_station: Station, - target_station: Station, -) -> RealizedStation: + start_station: MaterializedStation, + target_station: MaterializedStation, +) -> AppliedStation: """Compute the diff between two stations The same station must be described by both inputs, i.e. the assets, busbars and couplers (except for their open state) @@ -1112,14 +1132,14 @@ def station_diff( Parameters ---------- - start_station : Station + start_station : MaterializedStation The starting station from which to start the diff - target_station : Station + target_station : MaterializedStation The ending station to which the diff shall lead when applied to the starting station Returns ------- - RealizedStation + AppliedStation The realized station containing the target station and the coupler, reassignment and disconnection diffs """ assert [s.grid_model_id for s in start_station.assets] == [s.grid_model_id for s in target_station.assets], ( @@ -1168,7 +1188,7 @@ def station_diff( if start_coupler.open != target_coupler.open: coupler_diff.append(target_coupler) - return RealizedStation( + return AppliedStation( station=target_station, coupler_diff=coupler_diff, reassignment_diff=reassignment_diff, @@ -1197,7 +1217,9 @@ def topology_diff( """ realized_stations = [ station_diff(start_station, target_station) - for (start_station, target_station) in zip(start_topo.stations, target_topo.stations, strict=True) + for (start_station, target_station) in zip( + start_topo.materialize_stations(), target_topo.materialize_stations(), strict=True + ) ] coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) return RealizedTopology( @@ -1208,7 +1230,9 @@ def topology_diff( ) -def order_station_assets(station: Station, asset_ids: list[str]) -> tuple[Station, list[str], list[str]]: +def order_station_assets( + station: MaterializedStation, asset_ids: list[str] +) -> tuple[MaterializedStation, list[str], list[str]]: """Orders the assets in a station according to a list of asset ids. If an asset is not found in the station, it will be added to the not_found list. @@ -1255,7 +1279,7 @@ def order_station_assets(station: Station, asset_ids: list[str]) -> tuple[Statio "asset_connectivity": asset_connectivity, } ) - Station.model_validate(station) + MaterializedStation.model_validate(station) return station, not_found, ignored @@ -1283,7 +1307,7 @@ def order_topology(topology: Topology, station_ids: list[str]) -> tuple[Topology not_found = [] for relevant_node in station_ids: found = False - for station in topology.stations: + for station in topology.raw_stations: if station.grid_model_id == relevant_node: new_stations.append(station) found = True @@ -1291,15 +1315,15 @@ def order_topology(topology: Topology, station_ids: list[str]) -> tuple[Topology if not found: not_found.append(relevant_node) - topology = topology.model_copy(update={"stations": new_stations}) + topology = copy_topology_with_updates(topology, new_stations, topology.assets, topology.asset_bays) return topology, not_found def fuse_coupler( - station: Station, + station: MaterializedStation, coupler_grid_model_id: str, copy_info_from: bool = True, -) -> Station: +) -> MaterializedStation: """Fuses a coupler by merging the adjacent busbars into one busbar. Parameters @@ -1411,15 +1435,15 @@ def _replace_int_id(coupler: BusbarCoupler) -> BusbarCoupler: "couplers": new_couplers, } ) - Station.model_validate(station) + MaterializedStation.model_validate(station) return station def fuse_all_couplers_with_type( - station: Station, + station: MaterializedStation, coupler_type: str, copy_info_from: bool = True, -) -> tuple[Station, list[BusbarCoupler]]: +) -> tuple[MaterializedStation, list[BusbarCoupler]]: """Fuses all couplers of a specific type in a station. This will also filter all duplicate couplers, as there might be an edge case in which a triangle of busbars is not @@ -1460,7 +1484,7 @@ def fuse_all_couplers_with_type( return station, fused_couplers -def find_station_by_id(stations: list[Station], station_id: str) -> Station: +def find_station_by_id(stations: list[MaterializedStation], station_id: str) -> MaterializedStation: """Find a station by its grid_model_id in a list of stations. Parameters diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py b/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py index 8bec8da44..ce3c3e5eb 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py @@ -43,7 +43,7 @@ from fsspec.implementations.local import LocalFileSystem from jaxtyping import Bool from pydantic import BaseModel, ConfigDict, model_validator -from toop_engine_interfaces.asset_topology import Station, Topology +from toop_engine_interfaces.asset_topology import MaterializedStation, Topology from toop_engine_interfaces.filesystem_helper import save_pydantic_model_fs from toop_engine_interfaces.nminus1_definition import GridElement @@ -113,7 +113,7 @@ class ActionSet(BaseModel): """A list of high voltage direct current lines that can be set as a remedial action. This is currently not implemented yet in the solver.""" - local_actions: list[Station] + local_actions: list[MaterializedStation] """A list of split/reconfiguration actions that affect exactly one substation. These are must be ordered by station, i.e. actions affecting the same station are next to each other. The grid_model_id of the station should be used to determine which substation it affects. Within a station, asset @@ -172,7 +172,7 @@ def _validate_station_diff_arrays(self) -> "StationDiffArray": return self -def validate_actions_grouped(actions: list[Station]) -> None: +def validate_actions_grouped(actions: list[MaterializedStation]) -> None: """Validate that actions are grouped by station grid model id. Parameters @@ -198,7 +198,7 @@ def validate_actions_grouped(actions: list[Station]) -> None: last_grid_model_id = grid_model_id -def _validate_station_diff_hypothesis(starting_station: Station, action: Station) -> None: +def _validate_station_diff_hypothesis(starting_station: MaterializedStation, action: MaterializedStation) -> None: """Validate that only coupler open states and switching table values differ. Parameters @@ -219,7 +219,7 @@ def _validate_station_diff_hypothesis(starting_station: Station, action: Station f"{starting_station.grid_model_id}." ) - def normalize_station(station: Station) -> dict[str, object]: + def normalize_station(station: MaterializedStation) -> dict[str, object]: station_data = station.model_dump(mode="json") station_data.pop("asset_switching_table", None) for coupler in station_data.get("couplers", []): @@ -325,7 +325,9 @@ def load_station_diff_fs(filesystem: AbstractFileSystem, diff_file_path: str | P return _load_station_diff_io(buffer) -def expand_single_station_diff_to_actions(starting_station: Station, station_diff: StationDiffArray) -> list[Station]: +def expand_single_station_diff_to_actions( + starting_station: MaterializedStation, station_diff: StationDiffArray +) -> list[MaterializedStation]: """Expand densely stored station diffs to a list of stations with the same format as in the action set. This only expands a single station diff, so it should be called once per station in the action set. @@ -362,7 +364,7 @@ def expand_single_station_diff_to_actions(starting_station: Station, station_dif return actions -def expand_station_diffs(starting_topology: Topology, station_diffs: list[StationDiffArray]) -> list[Station]: +def expand_station_diffs(starting_topology: Topology, station_diffs: list[StationDiffArray]) -> list[MaterializedStation]: """Expand densely stored station diffs to a list of stations with the same format as in the action set. This expands a list of station diffs, so it can be called once per action set. @@ -381,7 +383,7 @@ def expand_station_diffs(starting_topology: Topology, station_diffs: list[Statio list[Station] A list of stations, each corresponding to an action in the station diffs action dimension. """ - grid_model_id_to_station = {station.grid_model_id: station for station in starting_topology.stations} + grid_model_id_to_station = {station.grid_model_id: station for station in starting_topology.materialize_stations()} actions = [] for station_diff in station_diffs: starting_station = grid_model_id_to_station[station_diff.grid_model_id] @@ -390,7 +392,7 @@ def expand_station_diffs(starting_topology: Topology, station_diffs: list[Statio def compress_actions_to_station_diffs( - starting_topology: Topology, actions: list[Station], validate_diff_hypothesis: bool = False + starting_topology: Topology, actions: list[MaterializedStation], validate_diff_hypothesis: bool = False ) -> list[StationDiffArray]: """Compress a list of stations with the same format as in the action set to densely stored station diffs. @@ -429,7 +431,7 @@ def compress_actions_to_station_diffs( If validate_diff_hypothesis is True and the change between actions for the same station regards fields other than the coupler states and switching table. """ - grid_model_id_to_station = {station.grid_model_id: station for station in starting_topology.stations} + grid_model_id_to_station = {station.grid_model_id: station for station in starting_topology.materialize_stations()} station_diffs = {} for grid_model_id, group in itertools.groupby(actions, key=lambda action: action.grid_model_id): if grid_model_id not in grid_model_id_to_station: diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index 79703bba2..335463718 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -16,9 +16,11 @@ AssetBay, Busbar, BusbarCoupler, - Station, + MaterializedStation, + RawStation, SwitchableAsset, Topology, + topology_from_materialized_stations, ) from toop_engine_interfaces.asset_topology_helpers import ( filter_assets_by_type, @@ -33,7 +35,7 @@ def test_station() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -53,7 +55,7 @@ def test_station() -> None: with pytest.raises(ValidationError): # Wrong shape of switching table - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -77,7 +79,7 @@ def test_station() -> None: with pytest.raises(ValidationError): # Coupler references non-existing busbar - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -101,7 +103,7 @@ def test_station() -> None: with pytest.raises(ValidationError): # Coupler references non-existing busbar - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -125,7 +127,7 @@ def test_station() -> None: with pytest.raises(ValidationError): # Busbar int_id is not unique - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=1, grid_model_id="busbar2"), @@ -148,7 +150,7 @@ def test_station() -> None: ) with pytest.raises(ValidationError): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -185,7 +187,7 @@ def test_station() -> None: ) with pytest.raises(ValidationError): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -207,7 +209,7 @@ def test_station() -> None: grid_model_id="station1", ) - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), @@ -224,9 +226,30 @@ def test_station() -> None: grid_model_id="bus_id1", ) assert station is not None + assert not station.is_split() + + split_station = station.model_copy( + update={ + "busbars": [ + Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), + Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id2"), + ] + } + ) + assert split_station.is_split() + + station_with_empty_bus_id = station.model_copy( + update={ + "busbars": [ + Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), + Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id=""), + ] + } + ) + assert not station_with_empty_bus_id.is_split() with pytest.raises(ValidationError): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), @@ -265,7 +288,7 @@ def test_station_connectivity_tables(): asset_switching_table = np.array([[True, False, True], [False, True, False]]) asset_connectivity = np.array([[True, True, True], [True, True, True]]) grid_model_id = "station1" - station = Station( + station = MaterializedStation( busbars=busbars, couplers=couplers, assets=assets, @@ -279,7 +302,7 @@ def test_station_connectivity_tables(): # entry in asset_switching_table is not in asset_connectivity asset_switching_table = np.array([[True, False, True], [False, True, False]]) asset_connectivity = np.array([[True, True, True], [True, False, True]]) - station = Station( + station = MaterializedStation( busbars=busbars, couplers=couplers, assets=assets, @@ -290,7 +313,7 @@ def test_station_connectivity_tables(): asset_switching_table = np.array([[True, False, True], [False, True, False]]) asset_connectivity = np.array([[True, True, True], [True, True, True]]) - station = Station( + station = MaterializedStation( busbars=busbars, couplers=couplers, assets=assets, @@ -301,9 +324,45 @@ def test_station_connectivity_tables(): assert station is not None +def test_topology_station_is_split() -> None: + station = RawStation( + grid_model_id="bus_id1", + busbars=[ + Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), + Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), + ], + couplers=[], + asset_ids=["line1"], + asset_branch_ends=[None], + asset_bay_ids=[None], + asset_switching_table=np.array([[True], [False]]), + ) + assert not station.is_split() + + split_station = station.model_copy( + update={ + "busbars": [ + Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), + Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id2"), + ] + } + ) + assert split_station.is_split() + + station_with_empty_bus_id = station.model_copy( + update={ + "busbars": [ + Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), + Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id=""), + ] + } + ) + assert not station_with_empty_bus_id.is_split() + + def test_schema() -> None: # Schema generation works - schema = Station.model_json_schema() + schema = MaterializedStation.model_json_schema() assert schema is not None assert "busbars" in schema["properties"] assert "couplers" in schema["properties"] @@ -313,7 +372,7 @@ def test_schema() -> None: def test_serialize_station() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -331,13 +390,13 @@ def test_serialize_station() -> None: ) serialized = station.model_dump_json() - station2 = Station.model_validate_json(serialized) + station2 = MaterializedStation.model_validate_json(serialized) assert station == station2 def test_load_asset_topology() -> None: - station1 = Station( + station1 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -354,7 +413,7 @@ def test_load_asset_topology() -> None: grid_model_id="station1", ) - station2 = Station( + station2 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar3"), Busbar(int_id=2, grid_model_id="busbar4"), @@ -371,10 +430,13 @@ def test_load_asset_topology() -> None: grid_model_id="station2", ) - topology = Topology( + topology = topology_from_materialized_stations( + reference_topology=Topology( + topology_id="topology1", + raw_stations=[], + timestamp=datetime.now(), + ), stations=[station1, station2], - topology_id="topology1", - timestamp=datetime.now(), ) with tempfile.TemporaryDirectory() as tmpdirname: @@ -384,8 +446,187 @@ def test_load_asset_topology() -> None: assert topology == loaded_topology +def test_topology_extracts_assets_and_materializes_stations() -> None: + station = MaterializedStation( + busbars=[ + Busbar(int_id=1, grid_model_id="busbar1"), + Busbar(int_id=2, grid_model_id="busbar2"), + ], + couplers=[ + BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), + ], + assets=[ + SwitchableAsset( + grid_model_id="line1", + branch_end="from", + asset_bay=AssetBay( + asset_bay_id="station1::line1::bay", + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ), + ), + SwitchableAsset(grid_model_id="load1"), + ], + asset_switching_table=np.array([[True, False], [False, True]]), + grid_model_id="station1", + ) + + topology = topology_from_materialized_stations( + reference_topology=Topology( + topology_id="topology1", + raw_stations=[], + timestamp=datetime.now(), + ), + stations=[station], + ) + + assert topology.raw_stations == [ + RawStation( + grid_model_id="station1", + busbars=station.busbars, + couplers=station.couplers, + asset_ids=["line1", "load1"], + asset_branch_ends=["from", None], + asset_bay_ids=["station1::line1::bay", None], + asset_switching_table=np.array([[True, False], [False, True]]), + ) + ] + assert [asset.grid_model_id for asset in topology.assets] == ["line1", "load1"] + assert [asset_bay.asset_bay_id for asset_bay in topology.asset_bays] == ["station1::line1::bay"] + + materialized_station = topology.materialize_stations()[0] + assert materialized_station == station.model_copy( + update={ + "assets": [ + station.assets[0].model_copy(update={"asset_bay_id": "station1::line1::bay"}), + station.assets[1], + ] + } + ) + + +def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_two_station_views() -> None: + asset_from = SwitchableAsset(grid_model_id="line1", type="line", branch_end="from") + asset_to = SwitchableAsset(grid_model_id="line1", type="line", branch_end="to") + + station_from = MaterializedStation( + grid_model_id="station_from", + busbars=[Busbar(int_id=1, grid_model_id="busbar1")], + couplers=[], + assets=[asset_from], + asset_switching_table=np.array([[True]]), + ) + station_to = MaterializedStation( + grid_model_id="station_to", + busbars=[Busbar(int_id=1, grid_model_id="busbar2")], + couplers=[], + assets=[asset_to], + asset_switching_table=np.array([[True]]), + ) + + topology = topology_from_materialized_stations( + reference_topology=Topology( + topology_id="topology1", + raw_stations=[], + timestamp=datetime.now(), + ), + stations=[station_from, station_to], + ) + + assert [asset.grid_model_id for asset in topology.assets] == ["line1"] + assert topology.raw_stations[0].asset_ids == ["line1"] + assert topology.raw_stations[0].asset_branch_ends == ["from"] + assert topology.raw_stations[1].asset_ids == ["line1"] + assert topology.raw_stations[1].asset_branch_ends == ["to"] + assert topology.materialize_stations() == [station_from, station_to] + + +def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_station() -> None: + station_from = MaterializedStation( + grid_model_id="station_from", + busbars=[Busbar(int_id=1, grid_model_id="busbar1")], + couplers=[], + assets=[ + SwitchableAsset( + grid_model_id="line1", + type="line", + branch_end="from", + asset_bay=AssetBay(dv_switch_grid_model_id="dv_from", sr_switch_grid_model_id={"busbar1": "sr_from"}), + ) + ], + asset_switching_table=np.array([[True]]), + ) + station_to = MaterializedStation( + grid_model_id="station_to", + busbars=[Busbar(int_id=1, grid_model_id="busbar2")], + couplers=[], + assets=[ + SwitchableAsset( + grid_model_id="line1", + type="line", + branch_end="to", + asset_bay=AssetBay(dv_switch_grid_model_id="dv_to", sr_switch_grid_model_id={"busbar2": "sr_to"}), + ) + ], + asset_switching_table=np.array([[True]]), + ) + + topology = topology_from_materialized_stations( + reference_topology=Topology( + topology_id="topology1", + raw_stations=[], + timestamp=datetime.now(), + ), + stations=[station_from, station_to], + ) + + assert sorted(asset_bay.asset_bay_id for asset_bay in topology.asset_bays) == [ + "station_from::line1::bay", + "station_to::line1::bay", + ] + assert topology.raw_stations[0].asset_bay_ids == ["station_from::line1::bay"] + assert topology.raw_stations[1].asset_bay_ids == ["station_to::line1::bay"] + + +def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_occurrence() -> None: + station = MaterializedStation( + grid_model_id="station1", + busbars=[Busbar(int_id=1, grid_model_id="busbar1")], + couplers=[], + assets=[ + SwitchableAsset( + grid_model_id="line1", + type="line", + asset_bay=AssetBay(dv_switch_grid_model_id="dv1", sr_switch_grid_model_id={"busbar1": "sr1"}), + ), + SwitchableAsset( + grid_model_id="line1", + type="line", + asset_bay=AssetBay(dv_switch_grid_model_id="dv2", sr_switch_grid_model_id={"busbar1": "sr2"}), + ), + ], + asset_switching_table=np.array([[True, True]]), + ) + + topology = topology_from_materialized_stations( + reference_topology=Topology( + topology_id="topology1", + raw_stations=[], + timestamp=datetime.now(), + ), + stations=[station], + ) + + assert topology.raw_stations[0].asset_ids == ["line1", "line1"] + assert topology.raw_stations[0].asset_bay_ids == ["station1::line1::bay", "station1::line1::bay::1"] + assert sorted(asset_bay.asset_bay_id for asset_bay in topology.asset_bays) == [ + "station1::line1::bay", + "station1::line1::bay::1", + ] + + def test_filter_out_of_service() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -419,7 +660,7 @@ def test_filter_out_of_service() -> None: def test_has_transmission_line_switching() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -447,7 +688,7 @@ def test_has_transmission_line_switching() -> None: assert has_transmission_line_switching(station) is False - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -477,7 +718,7 @@ def test_has_transmission_line_switching() -> None: def test_filter_duplicate_couplers() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -520,7 +761,7 @@ def test_filter_duplicate_couplers() -> None: def test_filter_disconnected_busbars() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -558,7 +799,7 @@ def test_filter_disconnected_busbars() -> None: def test_filter_disconnected_busbars_sort_by_asset_count() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -598,7 +839,7 @@ def test_filter_disconnected_busbars_sort_by_asset_count() -> None: def test_select_one_for_multi_connected_assets() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -640,7 +881,7 @@ def test_select_one_for_multi_connected_assets() -> None: def test_filter_assets_by_type() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -744,7 +985,7 @@ def test_station_bay() -> None: grid_model_id = "station1" # test valid Station - station = Station( + station = MaterializedStation( busbars=busbars, couplers=couplers, assets=assets, @@ -765,7 +1006,7 @@ def test_station_bay() -> None: SwitchableAsset(grid_model_id="line3"), ] with pytest.raises(ValidationError, match="busbar_id busbar3 in asset line2 does not exist in busbars"): - station = Station( + station = MaterializedStation( busbars=busbars, couplers=couplers, assets=assets, diff --git a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py index 6530e5972..b111df9d0 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py +++ b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py @@ -12,7 +12,8 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, - Station, + MaterializedStation, + RawStation, SwitchableAsset, Topology, ) @@ -34,8 +35,83 @@ ) +def build_raw_station( + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + asset_ids: list[str], + asset_switching_table: np.ndarray, + asset_branch_ends: list[str | None] | None = None, + asset_bay_ids: list[str | None] | None = None, +) -> RawStation: + """Build a raw station from explicit raw-topology fields. + + Parameters + ---------- + grid_model_id : str + Identifier of the station in the grid model. + busbars : list[Busbar] + Busbars belonging to the station. + couplers : list[BusbarCoupler] + Couplers belonging to the station. + asset_ids : list[str] + Grid model ids of the assets connected to the station. + asset_switching_table : np.ndarray + Busbar-to-asset switching matrix for the station. + asset_branch_ends : list[str | None] | None, optional + Optional branch-end metadata aligned with ``asset_ids``. + asset_bay_ids : list[str | None] | None, optional + Optional asset-bay metadata aligned with ``asset_ids``. + + Returns + ------- + RawStation + Raw station representation suitable for topology construction in tests. + """ + return RawStation( + grid_model_id=grid_model_id, + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=couplers, + asset_ids=asset_ids, + asset_branch_ends=asset_branch_ends if asset_branch_ends is not None else [None] * len(asset_ids), + asset_bay_ids=asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), + asset_switching_table=asset_switching_table, + asset_connectivity=None, + model_log=None, + ) + + +def build_test_topology(topology_id: str, raw_stations: list[RawStation], assets: list[SwitchableAsset]) -> Topology: + """Build a topology from raw stations and topology-owned assets. + + Parameters + ---------- + topology_id : str + Identifier of the topology. + raw_stations : list[RawStation] + Raw stations that define the topology layout. + assets : list[SwitchableAsset] + Topology-owned assets referenced by the raw stations. + + Returns + ------- + Topology + Topology instance for helper-focused tests. + """ + return Topology( + topology_id=topology_id, + raw_stations=raw_stations, + assets=assets, + timestamp=datetime.now(), + ) + + def test_merge_stations(): - station1 = Station( + station1 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -51,7 +127,7 @@ def test_merge_stations(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = Station( + station2 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -101,7 +177,7 @@ def test_merge_stations(): def test_merge_stations_append_behavior(): - station1 = Station( + station1 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -117,7 +193,7 @@ def test_merge_stations_append_behavior(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = Station( + station2 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -167,7 +243,7 @@ def test_merge_stations_append_behavior(): def test_merge_stations_raise_behavior(): - station1 = Station( + station1 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -183,7 +259,7 @@ def test_merge_stations_raise_behavior(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = Station( + station2 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -208,7 +284,7 @@ def test_merge_stations_raise_behavior(): def test_merge_stations_with_new_station_append(): - station1 = Station( + station1 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -224,7 +300,7 @@ def test_merge_stations_with_new_station_append(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = Station( + station2 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -254,7 +330,7 @@ def test_merge_stations_with_new_station_append(): def test_get_connected_assets(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -318,7 +394,7 @@ def test_find_busbars_for_coupler(): def test_station_diff_no_changes(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -343,7 +419,7 @@ def test_station_diff_no_changes(): def test_station_diff_coupler_state_change(): - start_station = Station( + start_station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -375,7 +451,7 @@ def test_station_diff_coupler_state_change(): def test_station_diff_asset_reassignment(): - start_station = Station( + start_station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -405,7 +481,7 @@ def test_station_diff_asset_reassignment(): def test_station_diff_asset_disconnection(): - start_station = Station( + start_station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -435,7 +511,7 @@ def test_station_diff_asset_disconnection(): def test_station_diff_unsupported_reconnection(): - start_station = Station( + start_station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -461,18 +537,21 @@ def test_station_diff_unsupported_reconnection(): def test_topology_diff() -> None: - start_station_1 = Station( - busbars=[ - Busbar(int_id=1, grid_model_id="busbar1"), - Busbar(int_id=2, grid_model_id="busbar2"), - ], - couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), - ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - ], + station1_busbars = [ + Busbar(int_id=1, grid_model_id="busbar1"), + Busbar(int_id=2, grid_model_id="busbar2"), + ] + station1_assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + ] + station1_couplers = [ + BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), + ] + start_station_1 = MaterializedStation( + busbars=station1_busbars, + couplers=station1_couplers, + assets=station1_assets, asset_switching_table=np.array([[True, False], [False, True]]), grid_model_id="station1", ) @@ -484,20 +563,23 @@ def test_topology_diff() -> None: } ) - start_station_2 = Station( - busbars=[ - Busbar(int_id=1, grid_model_id="busbar1"), - Busbar(int_id=2, grid_model_id="busbar2"), - Busbar(int_id=3, grid_model_id="busbar3"), - ], - couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), - BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=False, grid_model_id="coupler2"), - ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - ], + station2_busbars = [ + Busbar(int_id=1, grid_model_id="busbar1"), + Busbar(int_id=2, grid_model_id="busbar2"), + Busbar(int_id=3, grid_model_id="busbar3"), + ] + station2_assets = [ + SwitchableAsset(grid_model_id="station2_line1"), + SwitchableAsset(grid_model_id="station2_line2"), + ] + station2_couplers = [ + BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), + BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=False, grid_model_id="coupler2"), + ] + start_station_2 = MaterializedStation( + busbars=station2_busbars, + couplers=station2_couplers, + assets=station2_assets, asset_switching_table=np.array([[True, False], [False, True], [False, False]]), grid_model_id="station2", ) @@ -508,9 +590,45 @@ def test_topology_diff() -> None: } ) - start_topology = Topology(topology_id="topology1", stations=[start_station_1, start_station_2], timestamp=datetime.now()) - target_topology = Topology( - topology_id="topology1", stations=[target_station_1, target_station_2], timestamp=datetime.now() + start_topology = build_test_topology( + "topology1", + raw_stations=[ + build_raw_station( + "station1", + station1_busbars, + station1_couplers, + ["line1", "line2"], + np.array([[True, False], [False, True]]), + ), + build_raw_station( + "station2", + station2_busbars, + station2_couplers, + ["station2_line1", "station2_line2"], + np.array([[True, False], [False, True], [False, False]]), + ), + ], + assets=station1_assets + station2_assets, + ) + target_topology = build_test_topology( + "topology1", + raw_stations=[ + build_raw_station( + "station1", + station1_busbars, + [station1_couplers[0].model_copy(update={"open": True})], + ["line1", "line2"], + np.array([[False, True], [True, False]]), + ), + build_raw_station( + "station2", + station2_busbars, + station2_couplers, + ["station2_line1", "station2_line2"], + np.array([[False, False], [False, True], [False, False]]), + ), + ], + assets=station1_assets + station2_assets, ) realized_topo = topology_diff(start_topology, target_topology) @@ -524,7 +642,7 @@ def test_topology_diff() -> None: def test_filter_out_of_service(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1", in_service=True), Busbar(int_id=2, grid_model_id="busbar2", in_service=False), @@ -555,7 +673,7 @@ def test_filter_out_of_service(): def test_filter_disconnected_busbars(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -583,7 +701,7 @@ def test_filter_disconnected_busbars(): def test_filter_disconnected_busbars_open_coupler(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -614,7 +732,7 @@ def test_filter_disconnected_busbars_open_coupler(): def test_reindex_busbars(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=10, grid_model_id="busbar1"), Busbar(int_id=20, grid_model_id="busbar2"), @@ -641,7 +759,7 @@ def test_reindex_busbars(): def test_has_transmission_line_switching(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -662,7 +780,7 @@ def test_has_transmission_line_switching(): def test_order_station_assets() -> None: - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -723,59 +841,89 @@ def test_order_station_assets() -> None: def test_order_topology() -> None: - station = station = Station( - busbars=[ - Busbar(int_id=1, grid_model_id="busbar1"), - Busbar(int_id=2, grid_model_id="busbar2"), - ], - couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), - ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - ], - asset_switching_table=np.array( - [ - [True, False, True, False, False], - [False, False, False, True, True], - ] - ), - grid_model_id="station1", + busbars = [ + Busbar(int_id=1, grid_model_id="busbar1"), + Busbar(int_id=2, grid_model_id="busbar2"), + ] + couplers = [ + BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), + ] + switching_table = np.array( + [ + [True, False, True, False, False], + [False, False, False, True, True], + ] ) - stations = [ - station, - station.model_copy(update={"grid_model_id": "station2"}), - station.model_copy(update={"grid_model_id": "station3"}), - station.model_copy(update={"grid_model_id": "station4"}), + raw_stations = [ + build_raw_station("station1", busbars, couplers, ["line1", "line2", "line3", "line4", "line5"], switching_table), + build_raw_station( + "station2", + busbars, + couplers, + ["station2_line1", "station2_line2", "station2_line3", "station2_line4", "station2_line5"], + switching_table, + ), + build_raw_station( + "station3", + busbars, + couplers, + ["station3_line1", "station3_line2", "station3_line3", "station3_line4", "station3_line5"], + switching_table, + ), + build_raw_station( + "station4", + busbars, + couplers, + ["station4_line1", "station4_line2", "station4_line3", "station4_line4", "station4_line5"], + switching_table, + ), ] - topology = Topology( - topology_id="topo-popo", - stations=stations, - timestamp=datetime.now(), + assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + SwitchableAsset(grid_model_id="line5"), + SwitchableAsset(grid_model_id="station2_line1"), + SwitchableAsset(grid_model_id="station2_line2"), + SwitchableAsset(grid_model_id="station2_line3"), + SwitchableAsset(grid_model_id="station2_line4"), + SwitchableAsset(grid_model_id="station2_line5"), + SwitchableAsset(grid_model_id="station3_line1"), + SwitchableAsset(grid_model_id="station3_line2"), + SwitchableAsset(grid_model_id="station3_line3"), + SwitchableAsset(grid_model_id="station3_line4"), + SwitchableAsset(grid_model_id="station3_line5"), + SwitchableAsset(grid_model_id="station4_line1"), + SwitchableAsset(grid_model_id="station4_line2"), + SwitchableAsset(grid_model_id="station4_line3"), + SwitchableAsset(grid_model_id="station4_line4"), + SwitchableAsset(grid_model_id="station4_line5"), + ] + topology = build_test_topology( + "topo-popo", + raw_stations=raw_stations, + assets=assets, ) ordered, not_found = order_topology(topology, ["station4", "station2", "station1", "station3"]) - station_ids = [station.grid_model_id for station in ordered.stations] + station_ids = [station.grid_model_id for station in ordered.raw_stations] assert station_ids == ["station4", "station2", "station1", "station3"] assert not_found == [] ordered, not_found = order_topology(topology, ["station4", "station2", "station1", "station3", "station5"]) - station_ids = [station.grid_model_id for station in ordered.stations] + station_ids = [station.grid_model_id for station in ordered.raw_stations] assert station_ids == ["station4", "station2", "station1", "station3"] assert not_found == ["station5"] ordered, not_found = order_topology(topology, ["station4", "station2", "station1"]) - station_ids = [station.grid_model_id for station in ordered.stations] + station_ids = [station.grid_model_id for station in ordered.raw_stations] assert station_ids == ["station4", "station2", "station1"] assert not_found == [] def test_fuse_coupler(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -849,7 +997,7 @@ def test_fuse_coupler(): def test_fuse_all_couplers_with_type(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -928,7 +1076,7 @@ def test_fuse_all_couplers_with_type(): def test_filter_duplicate_couplers(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -954,7 +1102,7 @@ def test_filter_duplicate_couplers(): def test_filter_duplicate_couplers_with_type_hierarchy(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -998,7 +1146,7 @@ def test_filter_duplicate_couplers_with_type_hierarchy(): def test_filter_duplicate_couplers_no_duplicates(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1022,7 +1170,7 @@ def test_filter_duplicate_couplers_no_duplicates(): def test_filter_duplicate_couplers_with_unknown_type(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1051,7 +1199,7 @@ def test_filter_duplicate_couplers_with_unknown_type(): def test_filter_duplicate_couplers_multiple_duplicates(): - station = Station( + station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index ee23761b6..fea7893e2 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -12,7 +12,14 @@ import pytest from fsspec.implementations.dirfs import DirFileSystem from pydantic import ValidationError -from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, Station, SwitchableAsset, Topology +from toop_engine_interfaces.asset_topology import ( + Busbar, + BusbarCoupler, + MaterializedStation, + RawStation, + SwitchableAsset, + Topology, +) from toop_engine_interfaces.stored_action_set import ( ActionSet, StationDiffArray, @@ -27,6 +34,56 @@ ) +def build_raw_station( + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + asset_ids: list[str], + asset_switching_table: np.ndarray, + asset_branch_ends: list[str | None] | None = None, + asset_bay_ids: list[str | None] | None = None, +) -> RawStation: + """Build a raw station from explicit raw-topology fields. + + Parameters + ---------- + grid_model_id : str + Identifier of the station in the grid model. + busbars : list[Busbar] + Busbars belonging to the station. + couplers : list[BusbarCoupler] + Couplers belonging to the station. + asset_ids : list[str] + Grid model ids of the assets connected to the station. + asset_switching_table : np.ndarray + Busbar-to-asset switching matrix for the station. + asset_branch_ends : list[str | None] | None, optional + Optional branch-end metadata aligned with ``asset_ids``. + asset_bay_ids : list[str | None] | None, optional + Optional asset-bay metadata aligned with ``asset_ids``. + + Returns + ------- + RawStation + Raw station representation suitable for topology construction in tests. + """ + return RawStation.model_construct( + grid_model_id=grid_model_id, + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=couplers, + asset_ids=asset_ids, + asset_branch_ends=asset_branch_ends if asset_branch_ends is not None else [None] * len(asset_ids), + asset_bay_ids=asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), + asset_switching_table=asset_switching_table, + asset_connectivity=None, + model_log=None, + ) + + class DummyStation: def __init__(self, grid_model_id): self.grid_model_id = str(grid_model_id) @@ -206,7 +263,7 @@ def test_store_and_load_station_diff_io_preserves_station_order(tmp_path: Path) def test_validate_actions_grouped_accepts_grouped_actions(): - station_s1 = Station.model_construct( + station_s1 = MaterializedStation.model_construct( grid_model_id="s1", name=None, type=None, @@ -227,7 +284,7 @@ def test_validate_actions_grouped_accepts_grouped_actions(): def test_validate_actions_grouped_raises_for_non_grouped_actions(): - station_s1 = Station.model_construct( + station_s1 = MaterializedStation.model_construct( grid_model_id="s1", name=None, type=None, @@ -269,7 +326,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): ) ] - station_a = Station.model_construct( + station_a = MaterializedStation.model_construct( grid_model_id="station_a", name=None, type=None, @@ -282,13 +339,50 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): asset_connectivity=None, model_log=None, ) - station_b = station_a.model_copy(update={"grid_model_id": "station_b"}) + station_b = station_a.model_copy( + update={ + "grid_model_id": "station_b", + "assets": [station_a.assets[0].model_copy(update={"grid_model_id": "station_b_asset_0"})], + } + ) - starting_topology = Topology.model_construct( + starting_topology = Topology( topology_id="starting_topology", grid_model_file=None, name=None, - stations=[station_a, station_b], + raw_stations=[ + RawStation( + grid_model_id="station_a", + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=[], + asset_ids=["station_a_asset_0"], + asset_branch_ends=[None], + asset_bay_ids=[None], + asset_switching_table=np.zeros((1, 1), dtype=bool), + asset_connectivity=None, + model_log=None, + ), + RawStation( + grid_model_id="station_b", + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=[], + asset_ids=["station_b_asset_0"], + asset_branch_ends=[None], + asset_bay_ids=[None], + asset_switching_table=np.zeros((1, 1), dtype=bool), + asset_connectivity=None, + model_log=None, + ), + ], + assets=station_a.assets + station_b.assets, asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -310,25 +404,50 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): def test_compress_and_expand_station_diffs_random_roundtrip(): rng = np.random.default_rng(20260313) - starting_stations: list[Station] = [] - actions: list[Station] = [] - expected_by_station: dict[str, list[Station]] = {} + starting_stations: list[MaterializedStation] = [] + starting_raw_stations: list[RawStation] = [] + starting_assets: list[SwitchableAsset] = [] + actions: list[MaterializedStation] = [] + expected_by_station: dict[str, list[MaterializedStation]] = {} n_stations = 4 for station_idx in range(n_stations): grid_model_id = f"station_{station_idx}" - n_busbars = int(rng.integers(1, 5)) + n_busbars = int(rng.integers(2, 5)) n_assets = int(rng.integers(1, 7)) n_couplers = int(rng.integers(1, 6)) n_actions = int(rng.integers(1, 6)) + busbars = [ + Busbar.model_construct( + grid_model_id=f"{grid_model_id}_busbar_{busbar_idx}", + type=None, + name=None, + int_id=busbar_idx, + in_service=True, + bus_branch_bus_id=None, + ) + for busbar_idx in range(n_busbars) + ] + assets = [ + SwitchableAsset.model_construct( + grid_model_id=f"{grid_model_id}_asset_{asset_idx}", + type=None, + name=None, + in_service=True, + branch_end=None, + asset_bay=None, + ) + for asset_idx in range(n_assets) + ] + starting_couplers = [ BusbarCoupler.model_construct( grid_model_id=f"{grid_model_id}_coupler_{coupler_idx}", type=None, name=None, - busbar_from_id=0, - busbar_to_id=0, + busbar_from_id=coupler_idx % n_busbars, + busbar_to_id=(coupler_idx + 1) % n_busbars, open=bool(rng.integers(0, 2)), in_service=True, asset_bay=None, @@ -336,22 +455,34 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): for coupler_idx in range(n_couplers) ] - starting_station = Station.model_construct( + starting_switching_table = rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool) + starting_raw_stations.append( + build_raw_station( + grid_model_id, + busbars, + starting_couplers, + [asset.grid_model_id for asset in assets], + starting_switching_table, + ) + ) + starting_assets.extend(assets) + + starting_station = MaterializedStation.model_construct( grid_model_id=grid_model_id, name=None, type=None, region=None, voltage_level=None, - busbars=[], + busbars=busbars, couplers=starting_couplers, - assets=[], - asset_switching_table=rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool), + assets=assets, + asset_switching_table=starting_switching_table, asset_connectivity=None, model_log=None, ) starting_stations.append(starting_station) - station_actions: list[Station] = [] + station_actions: list[MaterializedStation] = [] for _ in range(n_actions): couplers = [ coupler.model_copy(update={"open": bool(rng.integers(0, 2))}) for coupler in starting_station.couplers @@ -369,11 +500,12 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): expected_by_station[grid_model_id] = station_actions actions.extend(station_actions) - starting_topology = Topology.model_construct( + starting_topology = Topology( topology_id="starting_topology", grid_model_file=None, name=None, - stations=starting_stations, + raw_stations=starting_raw_stations, + assets=starting_assets, asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -382,7 +514,7 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): station_diffs = compress_actions_to_station_diffs(starting_topology, actions) expanded_actions = expand_station_diffs(starting_topology, station_diffs) - result_by_station: dict[str, list[Station]] = {grid_model_id: [] for grid_model_id in expected_by_station} + result_by_station: dict[str, list[MaterializedStation]] = {grid_model_id: [] for grid_model_id in expected_by_station} for action in expanded_actions: result_by_station[action.grid_model_id].append(action) @@ -400,58 +532,89 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): - starting_station = Station.model_construct( - grid_model_id="station_x", - name=None, - type=None, - region=None, - voltage_level=None, - busbars=[], - couplers=[ - BusbarCoupler.model_construct( - grid_model_id="station_x_coupler_0", - type=None, - name=None, - busbar_from_id=1, - busbar_to_id=2, - open=False, - in_service=True, - asset_bay=None, - ) - ], - assets=[ - SwitchableAsset.model_construct( - grid_model_id="station_save_load_asset_1", - type=None, - name=None, - in_service=True, - branch_end=None, - asset_bay=None, - ), - SwitchableAsset.model_construct( - grid_model_id="station_save_load_asset_2", - type=None, - name=None, - in_service=True, - branch_end=None, - asset_bay=None, - ), - ], - asset_switching_table=np.array([[True, False], [False, True]], dtype=bool), - asset_connectivity=None, - model_log=None, - ) + busbars = [ + Busbar.model_construct( + grid_model_id="station_x_busbar_1", + type=None, + name=None, + int_id=1, + in_service=True, + bus_branch_bus_id=None, + ), + Busbar.model_construct( + grid_model_id="station_x_busbar_2", + type=None, + name=None, + int_id=2, + in_service=True, + bus_branch_bus_id=None, + ), + ] + couplers = [ + BusbarCoupler.model_construct( + grid_model_id="station_x_coupler_0", + type=None, + name=None, + busbar_from_id=1, + busbar_to_id=2, + open=False, + in_service=True, + asset_bay=None, + ) + ] + assets = [ + SwitchableAsset.model_construct( + grid_model_id="station_save_load_asset_1", + type=None, + name=None, + in_service=True, + branch_end=None, + asset_bay=None, + ), + SwitchableAsset.model_construct( + grid_model_id="station_save_load_asset_2", + type=None, + name=None, + in_service=True, + branch_end=None, + asset_bay=None, + ), + ] + asset_switching_table = np.array([[True, False], [False, True]], dtype=bool) - starting_topology = Topology.model_construct( + starting_topology = Topology( topology_id="starting_topology", grid_model_file=None, name=None, - stations=[starting_station], + raw_stations=[ + build_raw_station( + "station_x", + busbars, + couplers, + [asset.grid_model_id for asset in assets], + asset_switching_table, + ) + ], + assets=assets, asset_setpoints=None, timestamp=datetime.now(), metrics=None, ) + starting_station = MaterializedStation.model_construct( + grid_model_id="station_x", + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=couplers, + assets=assets, + asset_switching_table=asset_switching_table, + asset_connectivity=None, + model_log=None, + ) + valid_action = starting_station.model_copy( update={ "couplers": [ @@ -481,75 +644,89 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): - starting_station = Station.model_construct( - grid_model_id="station1", - name=None, - type=None, - region=None, - voltage_level=None, - busbars=[ - Busbar.model_construct( - grid_model_id="busbar1", - type=None, - name=None, - int_id=1, - in_service=True, - bus_branch_bus_id=None, - ), - Busbar.model_construct( - grid_model_id="busbar2", - type=None, - name=None, - int_id=2, - in_service=True, - bus_branch_bus_id=None, - ), - ], - couplers=[ - BusbarCoupler.model_construct( - grid_model_id="coupler1", - type=None, - name=None, - busbar_from_id=1, - busbar_to_id=2, - open=False, - in_service=True, - asset_bay=None, - ) - ], - assets=[ - SwitchableAsset.model_construct( - grid_model_id="asset1", - type=None, - name=None, - in_service=True, - branch_end=None, - asset_bay=None, - ), - SwitchableAsset.model_construct( - grid_model_id="asset2", - type=None, - name=None, - in_service=True, - branch_end=None, - asset_bay=None, - ), - ], - asset_switching_table=np.array([[True, False], [False, True]], dtype=bool), - asset_connectivity=None, - model_log=None, - ) + busbars = [ + Busbar.model_construct( + grid_model_id="busbar1", + type=None, + name=None, + int_id=1, + in_service=True, + bus_branch_bus_id=None, + ), + Busbar.model_construct( + grid_model_id="busbar2", + type=None, + name=None, + int_id=2, + in_service=True, + bus_branch_bus_id=None, + ), + ] + couplers = [ + BusbarCoupler.model_construct( + grid_model_id="coupler1", + type=None, + name=None, + busbar_from_id=1, + busbar_to_id=2, + open=False, + in_service=True, + asset_bay=None, + ) + ] + assets = [ + SwitchableAsset.model_construct( + grid_model_id="asset1", + type=None, + name=None, + in_service=True, + branch_end=None, + asset_bay=None, + ), + SwitchableAsset.model_construct( + grid_model_id="asset2", + type=None, + name=None, + in_service=True, + branch_end=None, + asset_bay=None, + ), + ] + asset_switching_table = np.array([[True, False], [False, True]], dtype=bool) - starting_topology = Topology.model_construct( + starting_topology = Topology( topology_id="starting_topology", grid_model_file=None, name=None, - stations=[starting_station], + raw_stations=[ + build_raw_station( + "station1", + busbars, + couplers, + [asset.grid_model_id for asset in assets], + asset_switching_table, + ) + ], + assets=assets, asset_setpoints=None, timestamp=datetime.now(), metrics=None, ) + starting_station = MaterializedStation.model_construct( + grid_model_id="station1", + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=couplers, + assets=assets, + asset_switching_table=asset_switching_table, + asset_connectivity=None, + model_log=None, + ) + local_action = starting_station.model_copy( update={ "couplers": [starting_station.couplers[0].model_copy(update={"open": True})], diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index b05b61746..2c931a2c9 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -12,7 +12,14 @@ import numpy as np import pytest from fsspec.implementations.local import LocalFileSystem -from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, Station, SwitchableAsset, Topology +from toop_engine_interfaces.asset_topology import ( + Busbar, + BusbarCoupler, + MaterializedStation, + RawStation, + SwitchableAsset, + Topology, +) from toop_engine_interfaces.filesystem_helper import save_pydantic_model_fs from toop_engine_interfaces.stored_action_set import ActionSet, load_action_set, load_action_set_fs, save_action_set @@ -46,8 +53,9 @@ def _build_large_random_action_set( ) asset_counts = np.tile(asset_counts, n_stations // len(asset_counts)) - stations: list[Station] = [] - local_actions: list[Station] = [] + raw_stations: list[RawStation] = [] + topology_assets: list[SwitchableAsset] = [] + local_actions: list[MaterializedStation] = [] for station_idx in range(n_stations): grid_model_id = f"station_{station_idx:03d}" @@ -92,7 +100,27 @@ def _build_large_random_action_set( for asset_idx in range(n_assets) ] - starting_station = Station.model_construct( + asset_switching_table = rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool) + + raw_station = RawStation.model_construct( + grid_model_id=grid_model_id, + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=couplers, + asset_ids=[asset.grid_model_id for asset in assets], + asset_branch_ends=[asset.branch_end for asset in assets], + asset_bay_ids=[asset.asset_bay_id for asset in assets], + asset_switching_table=asset_switching_table, + asset_connectivity=None, + model_log=None, + ) + raw_stations.append(raw_station) + topology_assets.extend(assets) + + starting_station = MaterializedStation.model_construct( grid_model_id=grid_model_id, name=None, type=None, @@ -101,11 +129,10 @@ def _build_large_random_action_set( busbars=busbars, couplers=couplers, assets=assets, - asset_switching_table=rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool), + asset_switching_table=asset_switching_table, asset_connectivity=None, model_log=None, ) - stations.append(starting_station) base_switching_table = np.asarray(starting_station.asset_switching_table, dtype=bool) for _ in range(actions_per_station): @@ -128,11 +155,12 @@ def _build_large_random_action_set( ) ) - starting_topology = Topology.model_construct( + starting_topology = Topology( topology_id="performance_starting_topology", grid_model_file=None, name=None, - stations=stations, + raw_stations=raw_stations, + assets=topology_assets, asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -175,11 +203,12 @@ def test_stored_action_set_large_performance(tmp_path: Path, record_property) -> ) # Sanity checks for requested test configuration. - assert len(action_set.starting_topology.stations) == n_stations + starting_stations = action_set.starting_topology.materialize_stations() + assert len(starting_stations) == n_stations assert len(action_set.local_actions) == n_actions - mean_assets = float(np.mean([len(station.assets) for station in action_set.starting_topology.stations])) + mean_assets = float(np.mean([len(station.assets) for station in starting_stations])) assert mean_assets == avg_assets_per_station - assert all(len(station.couplers) == couplers_per_station for station in action_set.starting_topology.stations) + assert all(len(station.couplers) == couplers_per_station for station in starting_stations) old_file = tmp_path / "action_set_legacy.json" new_json = tmp_path / "action_set_split.json" From 59e985bbeee8fffb3d6650882a74ce24b035e8ad Mon Sep 17 00:00:00 2001 From: LeonHilf <149764770+LeonHilf@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:44:43 +0200 Subject: [PATCH 02/24] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: LeonHilf <149764770+LeonHilf@users.noreply.github.com> --- packages/interfaces_pkg/tests/test_stored_action_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index fea7893e2..27d1122e0 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -67,7 +67,7 @@ def build_raw_station( RawStation Raw station representation suitable for topology construction in tests. """ - return RawStation.model_construct( + return RawStation( grid_model_id=grid_model_id, name=None, type=None, From cb8dfe6c2ffa38b40dcd4b0b95ad21b086665663 Mon Sep 17 00:00:00 2001 From: LeonHilf <149764770+LeonHilf@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:44:57 +0200 Subject: [PATCH 03/24] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: LeonHilf <149764770+LeonHilf@users.noreply.github.com> --- packages/interfaces_pkg/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interfaces_pkg/README.md b/packages/interfaces_pkg/README.md index 2432f57de..23354956c 100644 --- a/packages/interfaces_pkg/README.md +++ b/packages/interfaces_pkg/README.md @@ -5,7 +5,7 @@ The `interfaces` package provides a set of abstractions and adapters to enable i ## Interfaces [Asset Topology][toop_engine_interfaces.asset_topology] - Defines the physical structure and configuration of electrical assets in the grid system. -Topology stores lean station records in `raw_stations`; rich `Station` objects are reconstructed with `Topology.materialize_stations()`. +Topology stores lean station records in `raw_stations`; rich `MaterializedStation` objects are reconstructed with `Topology.materialize_stations()`. A full description is found here: [Asset Topology](https://eliagroup.github.io/ToOp/interfaces/asset_topology/) The [`BackendInterface`][toop_engine_interfaces.backend.BackendInterface] is an abstract interface to read grid data for a bus-branch model. It provides raw numeric inputs for the solver — no validation or processing. Specifically not task of this interface is to perform any validations or processing of the data. From 5205fd59ef6bf79b1bec1f72d348da73db9eddd2 Mon Sep 17 00:00:00 2001 From: LeonHilf <149764770+LeonHilf@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:45:19 +0200 Subject: [PATCH 04/24] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: LeonHilf <149764770+LeonHilf@users.noreply.github.com> --- docs/interfaces/asset_topology.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/interfaces/asset_topology.md b/docs/interfaces/asset_topology.md index 07061d990..27a1700e7 100644 --- a/docs/interfaces/asset_topology.md +++ b/docs/interfaces/asset_topology.md @@ -72,15 +72,14 @@ See the [`Asset Topology Reference`][toop_engine_interfaces.asset_topology] for The intended station contract is bus-view based: -- `Station.grid_model_id` and `TopologyStation.grid_model_id` identify the bus-branch bus id of the splitable station view. +- `MaterializedStation.grid_model_id` and `RawStation.grid_model_id` identify the bus-branch bus id of the splitable station view. - The station busbars belong to that bus view and therefore must carry matching `bus_branch_bus_id` values. - The station-local asset arrays and switching tables describe which assets are visible in that station view and how they attach locally. The current importer implementations are not fully uniform yet: - The bus-breaker powsybl helper narrows busbars and assets to the selected `bus_id` before building the station view. -- The node-breaker powsybl importer currently assigns a bus-specific `Station.grid_model_id` but still derives the asset list from the full substation graph. This is broader than the intended "single bus-branch bus" scope and should be treated as current implementation behavior, not as the desired long-term contract. - +- The node-breaker powsybl importer currently assigns a bus-specific `MaterializedStation.grid_model_id` but still derives the asset list from the full substation graph. This is broader than the intended "single bus-branch bus" scope and should be treated as current implementation behavior, not as the desired long-term contract. To populate Asset Topology data from grid models, use the [`Network Graph module`][toop_engine_importer.network_graph]. A Pandapower to Asset Topology implementation is found in the importer: [`get_list_of_stations_ids`][toop_engine_importer.pandapower_import.asset_topology.get_list_of_stations_ids] From c8b609d344781cd08f5d373bfdfc64ea79585116 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:47:36 +0000 Subject: [PATCH 05/24] docs: fix asset topology class references --- docs/interfaces/asset_topology.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/interfaces/asset_topology.md b/docs/interfaces/asset_topology.md index 27a1700e7..12fdab6b2 100644 --- a/docs/interfaces/asset_topology.md +++ b/docs/interfaces/asset_topology.md @@ -10,14 +10,14 @@ Asset Topology is essential when stations do not allow free assignment of lines Collection of time steps, each represented by a [`Topology`][toop_engine_interfaces.asset_topology.Topology]. - [`Topology`][toop_engine_interfaces.asset_topology.Topology] - Stores lean [`TopologyStation`][toop_engine_interfaces.asset_topology.TopologyStation] records in `raw_stations`, topology-owned canonical assets in `assets`, and optional [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] objects. Rich [`Station`][toop_engine_interfaces.asset_topology.Station] objects are reconstructed with [`Topology.materialize_stations()`][toop_engine_interfaces.asset_topology.Topology.materialize_stations]. + Stores lean [`RawStation`][toop_engine_interfaces.asset_topology.RawStation] records in `raw_stations`, topology-owned canonical assets in `assets`, and optional [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] objects. Rich [`MaterializedStation`][toop_engine_interfaces.asset_topology.MaterializedStation] objects are reconstructed with [`Topology.materialize_stations()`][toop_engine_interfaces.asset_topology.Topology.materialize_stations]. -- [`Station`][toop_engine_interfaces.asset_topology.Station] +- [`MaterializedStation`][toop_engine_interfaces.asset_topology.MaterializedStation] Contains lists of [`Busbar`][toop_engine_interfaces.asset_topology.Busbar], [`BusbarCoupler`][toop_engine_interfaces.asset_topology.BusbarCoupler], and [`SwitchableAsset`][toop_engine_interfaces.asset_topology.SwitchableAsset]. Includes `asset_switching_table`, the current switch connection layout and `asset_connectivity`, all possible selections. The `grid_model_id` refers to the bus-branch bus id of the splitable station view, not to the full voltage level id. -- [`TopologyStation`][toop_engine_interfaces.asset_topology.TopologyStation] +- [`RawStation`][toop_engine_interfaces.asset_topology.RawStation] Stores the lean station representation used inside [`Topology`][toop_engine_interfaces.asset_topology.Topology]. Instead of embedded asset payloads it stores aligned station-local arrays `asset_ids`, `asset_branch_ends`, and `asset_bay_ids`. From a958c1758a5050d2901f5272459d7ef5a8b47d20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:48:38 +0000 Subject: [PATCH 06/24] docs: fix asset topology class references --- docs/interfaces/asset_topology.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/interfaces/asset_topology.md b/docs/interfaces/asset_topology.md index 12fdab6b2..da026f397 100644 --- a/docs/interfaces/asset_topology.md +++ b/docs/interfaces/asset_topology.md @@ -29,7 +29,7 @@ Asset Topology is essential when stations do not allow free assignment of lines Note: the current implementation only supports a busbar connection between two busbars. It is planned to add an asset bay for both connection sides. - [`SwitchableAsset`][toop_engine_interfaces.asset_topology.SwitchableAsset] - Represents an asset (line, transformer, generator, etc.) that can be switched. You may leave out non-switchable assets or assign them to a single busbar to have a complete representation of the physical [`Station`][toop_engine_interfaces.asset_topology.Station]. + Represents an asset (line, transformer, generator, etc.) that can be switched. You may leave out non-switchable assets or assign them to a single busbar to have a complete representation of the physical [`MaterializedStation`][toop_engine_interfaces.asset_topology.MaterializedStation]. - [`AssetBay`][toop_engine_interfaces.asset_topology.AssetBay] Describes the physical connection (switches) between an asset and busbars. It may contain breaker and disconnector switches. From 22609c0130eaac7972c88d99338aefa012d25d21 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 8 Jun 2026 14:07:44 +0200 Subject: [PATCH 07/24] fix: comments Signed-off-by: Leonard Hilfrich --- .../toop_engine_dc_solver/example_classes.py | 8 +- .../export/disconnection_switch_updates.py | 6 +- .../export/station_switch_updates.py | 2 +- .../postprocess/apply_asset_topo_powsybl.py | 5 +- .../postprocess/postprocess_pandapower.py | 5 +- .../postprocess/postprocess_powsybl.py | 5 +- .../preprocess/preprocess_switching.py | 4 +- packages/dc_solver_pkg/tests/conftest.py | 8 +- .../initial_topology/asset_topology.json | 4 +- .../test_preprocess_bb_outage.py | 19 +- .../test_preprocess_switching.py | 6 +- .../powsybl/powsybl_asset_topo.py | 28 +-- .../network_graph/graph_to_asset_topo.py | 9 +- .../network_graph/powsybl_station_to_graph.py | 3 + .../pandapower_import/asset_topology.py | 19 ++ .../network_graph/test_graph_to_asset_topo.py | 14 +- .../test_powsybl_station_to_graph.py | 48 ++--- .../test_asset_topology_pandapower.py | 8 + .../pypowsybl_import/test_preprocessing.py | 4 +- .../toop_engine_interfaces/asset_topology.py | 164 ++++++++++++------ .../asset_topology_helpers.py | 67 +++---- .../tests/test_asset_topology.py | 80 +++++++-- .../tests/test_asset_topology_helpers.py | 6 +- .../tests/test_stored_action_set.py | 22 +-- .../test_stored_action_set_performance.py | 4 +- 25 files changed, 359 insertions(+), 189 deletions(-) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py index d5b43184c..c70cfb904 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py @@ -56,16 +56,16 @@ def get_basic_node_breaker_topology() -> Topology: ) ], asset_ids=["L4", "L5", "L8"], - asset_branch_ends=[None, None, None], + asset_terminals=[None, None, None], asset_bay_ids=["VL4_0::L4::bay", "VL4_0::L5::bay", "VL4_0::L8::bay"], asset_switching_table=np.array([[False, False, False], [True, True, False]]), asset_connectivity=np.array([[True, True, True], [True, True, True]]), ) ], assets=[ - SwitchableAsset(grid_model_id="L4", type="LINE", name="", in_service=True, branch_end=None), - SwitchableAsset(grid_model_id="L5", type="LINE", name="", in_service=True, branch_end=None), - SwitchableAsset(grid_model_id="L8", type="LINE", name="", in_service=True, branch_end=None), + SwitchableAsset(grid_model_id="L4", type="LINE", name="", in_service=True), + SwitchableAsset(grid_model_id="L5", type="LINE", name="", in_service=True), + SwitchableAsset(grid_model_id="L8", type="LINE", name="", in_service=True), ], asset_bays=[ AssetBay( diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py index 0d723e24f..4d6fbb805 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py @@ -47,10 +47,10 @@ def get_disconnected_asset_ids( disconnection_map: dict[str, GridElement] = {disconnection.id: disconnection for disconnection in disconnections} disconnection_asset_map: dict[str, list[AssetBay]] = {disconnection.id: [] for disconnection in disconnections} for station in stations: - for asset in station.assets: - if asset.grid_model_id in disconnection_map and asset.asset_bay is not None: + for asset, asset_bay in zip(station.assets, station.asset_bays, strict=True): + if asset.grid_model_id in disconnection_map and asset_bay is not None: corresponding_disconnection = disconnection_map[asset.grid_model_id] - disconnection_asset_map[corresponding_disconnection.id].append(asset.asset_bay) + disconnection_asset_map[corresponding_disconnection.id].append(asset_bay) return disconnection_asset_map diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py index 269031aff..9436b845d 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py @@ -171,7 +171,7 @@ def _get_asset_switch_diffs( diff_switches: list[dict[str, str | bool]] = [] for column, changed_asset in enumerate(changed_station.assets): - asset_bay = changed_asset.asset_bay + asset_bay = changed_station.asset_bays[column] if asset_bay is None: continue changed_switch_states = changed_switching_table[:, column] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index de2ac6125..1f6106478 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -59,9 +59,8 @@ def get_coupler_states_from_busbar_couplers(station_couplers: list[BusbarCoupler def get_asset_bay_grid_model_id_list(station: MaterializedStation) -> list[dict[str, str] | None]: """Get selector switch ids for each station asset.""" - asset_bays = [asset.asset_bay for asset in station.assets] sr_switch_grid_model_id_list: list[dict[str, str] | None] = [] - for asset_bay in asset_bays: + for asset_bay in station.asset_bays: if asset_bay is None or asset_bay.sr_switch_grid_model_id is None: sr_switch_grid_model_id_list.append(None) else: @@ -105,7 +104,7 @@ def get_asset_switch_states_from_station( } ) elif active_busbars == 0: - asset_bay = station.assets[column].asset_bay + asset_bay = station.asset_bays[column] assert asset_bay is not None switch_disconnection_list.append( { diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py index a5924afab..d017dd847 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py @@ -200,9 +200,10 @@ def compute_cross_coupler_flows( if station.asset_switching_table[busbars_a, index].any(): # The asset is on busbar A, include it if asset.is_branch() is True: - if asset.branch_end is None: + branch_end = station.asset_terminals[index] + if branch_end is None: raise ValueError("Branch end must be set in asset topo") - from_end = asset.branch_end in ("from", "hv") + from_end = branch_end in ("from", "hv") p_on_a += get_pandapower_branch_loadflow_results_sequence( net, types=[asset_type], ids=[asset_id], measurement="active", from_end=from_end, adjust_signs=False ).item() diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index 294e0a7bd..507c5a0ea 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -270,9 +270,10 @@ def compute_cross_coupler_flows( if station.asset_switching_table[busbars_a, index].any(): # The asset is on busbar A, include it if asset.is_branch() is True: - if asset.branch_end is None: + branch_end = station.asset_terminals[index] + if branch_end is None: raise ValueError("Branch end is None") - from_end = asset.branch_end in ("from", "hv") + from_end = branch_end in ("from", "hv") p, q = extract_single_branch_loadflow_result(branch_res, asset.grid_model_id, from_end) p_sum += p q_sum += q diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py index c18996036..620208365 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py @@ -440,13 +440,13 @@ def add_missing_asset_topology_branch_info( new_raw_stations = [] for station in asset_topology.raw_stations: new_branch_ends = [] - for asset_id, branch_end in zip(station.asset_ids, station.asset_branch_ends, strict=True): + for asset_id, branch_end in zip(station.asset_ids, station.asset_terminals, strict=True): index = branch_id_lookup.get(asset_id, None) if index is not None and (overwrite_if_present or branch_end is None): new_branch_ends.append("from" if branch_from_nodes[index] == station.grid_model_id else "to") else: new_branch_ends.append(branch_end) - new_raw_stations.append(station.model_copy(update={"asset_branch_ends": new_branch_ends})) + new_raw_stations.append(station.model_copy(update={"asset_terminals": new_branch_ends})) return copy_topology_with_updates( reference_topology=asset_topology, diff --git a/packages/dc_solver_pkg/tests/conftest.py b/packages/dc_solver_pkg/tests/conftest.py index 1f1394982..df6c454f4 100644 --- a/packages/dc_solver_pkg/tests/conftest.py +++ b/packages/dc_solver_pkg/tests/conftest.py @@ -842,10 +842,10 @@ def basic_node_breaker_topology() -> Topology: @pytest.fixture(scope="session") def mock_station() -> MaterializedStation: - asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, branch_end="from", type="line") - asset2 = SwitchableAsset(grid_model_id="branch_02", in_service=True, branch_end="to", type="line") - asset3 = SwitchableAsset(grid_model_id="branch_03", in_service=True, branch_end="from", type="line") - asset4 = SwitchableAsset(grid_model_id="branch_04", in_service=True, branch_end="to", type="line") + asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, type="line") + asset2 = SwitchableAsset(grid_model_id="branch_02", in_service=True, type="line") + asset3 = SwitchableAsset(grid_model_id="branch_03", in_service=True, type="line") + asset4 = SwitchableAsset(grid_model_id="branch_04", in_service=True, type="line") # Create mock Busbar objects busbar_0 = Busbar(grid_model_id="busbar_0", int_id=1) 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 855e3e2a8..eb10486e8 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 @@ -118,7 +118,7 @@ "L8", "load1" ], - "asset_branch_ends": [ + "asset_terminals": [ null, null, null, @@ -275,7 +275,7 @@ "L9", "load2" ], - "asset_branch_ends": [ + "asset_terminals": [ null, null, null, diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py index 03e4f1810..176474300 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py @@ -72,9 +72,9 @@ def test_get_total_injection_along_stub_branch(network_data: NetworkData): def test_extract_outage_index_injection_from_asset(network_data: NetworkData): # Create mock SwitchableAsset objects - asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, branch_end="from", type="line") - asset2 = SwitchableAsset(grid_model_id="branch_12", in_service=False, branch_end="to", type="line") - asset3 = SwitchableAsset(grid_model_id="branch_23", in_service=True, branch_end="from", type="line") + asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, type="line") + asset2 = SwitchableAsset(grid_model_id="branch_12", in_service=False, type="line") + asset3 = SwitchableAsset(grid_model_id="branch_23", in_service=True, type="line") # asset4 = SwitchableAsset( # grid_model_id="branch_02", in_service=True, branch_end="from" # ) @@ -87,7 +87,6 @@ def test_extract_outage_index_injection_from_asset(network_data: NetworkData): asset7 = SwitchableAsset( grid_model_id="injection_node_2", in_service=True, - branch_end=None, type="GENERATOR", ) # asset8 = SwitchableAsset( @@ -178,21 +177,19 @@ def test_extract_outage_index_injection_from_asset(network_data: NetworkData): def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): # Create mock SwitchableAsset objects - asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, branch_end="from", type="line") - asset2 = SwitchableAsset(grid_model_id="branch_12", in_service=False, branch_end="to", type="line") - asset3 = SwitchableAsset(grid_model_id="branch_23", in_service=True, branch_end="from", type="line") - asset4 = SwitchableAsset(grid_model_id="branch_02", in_service=True, branch_end="from", type="line") - asset5 = SwitchableAsset(grid_model_id="branch_03", in_service=True, branch_end="from", type="line") + asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, type="line") + asset2 = SwitchableAsset(grid_model_id="branch_12", in_service=False, type="line") + asset3 = SwitchableAsset(grid_model_id="branch_23", in_service=True, type="line") + asset4 = SwitchableAsset(grid_model_id="branch_02", in_service=True, type="line") + asset5 = SwitchableAsset(grid_model_id="branch_03", in_service=True, type="line") asset6 = SwitchableAsset( grid_model_id="injection_node_0", in_service=True, - branch_end=None, type="GENERATOR", ) asset7 = SwitchableAsset( grid_model_id="injection_node_2", in_service=True, - branch_end=None, type="GENERATOR", ) # asset8 = SwitchableAsset( diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py index d93d22436..e48f0eb56 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py @@ -196,12 +196,12 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No from_ends = 0 to_ends = 0 for station in topo.materialize_stations(): - for asset in station.assets: + for index, asset in enumerate(station.assets): if asset.grid_model_id in network_data.branch_ids: assert asset.name in network_data.branch_names assert asset.type in network_data.branch_types - assert asset.branch_end in ["from", "to"] - if asset.branch_end == "from": + assert station.asset_terminals[index] in ["from", "to"] + if station.asset_terminals[index] == "from": from_ends += 1 else: to_ends += 1 diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index 438bf20ec..e1003a6c4 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -151,11 +151,11 @@ def get_list_of_switchable_assets_from_df( switchable_assets_dict = station_branches.to_dict(orient="records") if asset_bay_list is not None: for index, _ in enumerate(switchable_assets_dict): - switchable_assets_dict[index]["asset_bay"] = asset_bay_list[index] + switchable_assets_dict[index]["asset_bay_id"] = asset_bay_list[index].asset_bay_id elif asset_bay_dict is not None: for index, asset in enumerate(switchable_assets_dict): if asset["grid_model_id"] in asset_bay_dict: - switchable_assets_dict[index]["asset_bay"] = asset_bay_dict[asset["grid_model_id"]] + switchable_assets_dict[index]["asset_bay_id"] = asset_bay_dict[asset["grid_model_id"]].asset_bay_id switchable_assets_list = [SwitchableAsset(**switchable_asset) for switchable_asset in switchable_assets_dict] return switchable_assets_list @@ -459,11 +459,14 @@ def get_raw_stations_and_assets( station_topology.elements, station_buses, dangling_lines, element_names ) asset_connectivity = np.ones_like(switching_matrix, dtype=bool) + asset_terminals = ( + station_elements["branch_end"].tolist() + if "branch_end" in station_elements.columns + else [None] * len(station_elements) + ) assets = get_list_of_switchable_assets_from_df(station_elements) for asset in assets: - topology_assets.setdefault( - asset.grid_model_id, asset.model_copy(update={"branch_end": None, "asset_bay_id": None}) - ) + topology_assets.setdefault(asset.grid_model_id, asset.model_copy(update={"asset_bay_id": None})) station = RawStation( grid_model_id=bus_id, @@ -473,7 +476,7 @@ def get_raw_stations_and_assets( busbars=get_list_of_busbars_from_df(station_buses), couplers=get_list_of_coupler_from_df(coupler_elements), asset_ids=[asset.grid_model_id for asset in assets], - asset_branch_ends=[asset.branch_end for asset in assets], + asset_terminals=asset_terminals, asset_bay_ids=[None] * len(assets), asset_switching_table=switching_matrix, asset_connectivity=asset_connectivity, @@ -582,11 +585,11 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati for grid_model_id, switch in local_switches.iterrows() ] from_branch_assets = [ - SwitchableAsset(grid_model_id=grid_model_id, type=branch.type, branch_end="from") + SwitchableAsset(grid_model_id=grid_model_id, type=branch.type) for grid_model_id, branch in from_branches.iterrows() ] to_branch_assets = [ - SwitchableAsset(grid_model_id=grid_model_id, type=branch.type, branch_end="to") + SwitchableAsset(grid_model_id=grid_model_id, type=branch.type) for grid_model_id, branch in to_branches.iterrows() ] injection_assets = [ @@ -594,6 +597,9 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati for grid_model_id, injection in injections.iterrows() ] assets = from_branch_assets + to_branch_assets + injection_assets + asset_terminals = ( + ["from"] * len(from_branch_assets) + ["to"] * len(to_branch_assets) + [None] * len(injection_assets) + ) from_branch_bus_index = [busbar_mapper[branch.bus_breaker_bus1_id] for branch in from_branches.itertuples()] to_branch_bus_index = [busbar_mapper[branch.bus_breaker_bus2_id] for branch in to_branches.itertuples()] @@ -605,9 +611,7 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati switching_table[idx, asset_index] = True for asset in assets: - topology_assets.setdefault( - asset.grid_model_id, asset.model_copy(update={"branch_end": None, "asset_bay_id": None}) - ) + topology_assets.setdefault(asset.grid_model_id, asset.model_copy(update={"asset_bay_id": None})) station = RawStation( grid_model_id=bus_id, @@ -615,7 +619,7 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati busbars=busbars, couplers=couplers, asset_ids=[asset.grid_model_id for asset in assets], - asset_branch_ends=[asset.branch_end for asset in assets], + asset_terminals=asset_terminals, asset_bay_ids=[None] * len(assets), asset_switching_table=switching_table, ) 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 f024fcf74..7f5dc9170 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 @@ -25,6 +25,7 @@ ) from toop_engine_interfaces.asset_topology import ( AssetBay, + build_asset_bay_id, ) logger = structlog.get_logger(__name__) @@ -583,6 +584,7 @@ def get_dv_sr_switch(asset_bays_df: pd.DataFrame) -> tuple[dict[str, str], list[ def get_asset_bay( switches_df: pat.DataFrame[SwitchSchema], + station_grid_model_id: str, asset_grid_model_id: str, busbar_df: pd.DataFrame, edge_connection_info: dict[str, EdgeConnectionInfo], @@ -594,6 +596,8 @@ def get_asset_bay( switches_df: pat.DataFrame[SwitchSchema] Dataframe with all switches of the substation. expects NetworkGraphData.switches + station_grid_model_id: str + Station identifier owning the asset bay. asset_grid_model_id: str Asset grid model id for which the asset bays should be retrieved. busbar_df: pd.DataFrame @@ -653,7 +657,10 @@ def get_asset_bay( f" AssetBay ignored for grid_model_id: {asset_grid_model_id}" ) return None, logs - return AssetBay(**asset_bay_dict), logs + return AssetBay( + asset_bay_id=build_asset_bay_id(station_grid_model_id, asset_grid_model_id), + **asset_bay_dict, + ), logs def get_station_connection_tables( 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 40c7ccf06..8025774aa 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 @@ -360,6 +360,7 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list(): asset_bay, logs = get_asset_bay( graph_data.switches, + station_grid_model_id=substation_id, asset_grid_model_id=asset_grid_model_id, busbar_df=busbar_df, edge_connection_info=edge_connection_info, @@ -386,6 +387,7 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati couplers = get_list_of_coupler_from_df(coupler_df) assets = get_list_of_switchable_assets_from_df(station_branches=switchable_assets_df, asset_bay_dict=asset_bay_dict) remove_suffix_from_switchable_assets(assets) + asset_bays_by_id = {asset_bay.asset_bay_id: asset_bay for asset_bay in asset_bay_dict.values()} station = MaterializedStation( grid_model_id=bus_id, @@ -395,6 +397,7 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati busbars=busbars, couplers=couplers, assets=assets, + asset_bays=[asset_bays_by_id.get(asset.asset_bay_id) for asset in assets], asset_switching_table=asset_switching_table, asset_connectivity=asset_connectivity, busbar_switching_table=busbar_switching_table, diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index 80c7a6d94..86c2434d6 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -35,6 +35,7 @@ AssetBay, MaterializedStation, Topology, + build_asset_bay_id, topology_parts_from_materialized_station, ) @@ -248,6 +249,8 @@ def get_branches_from_station( # noqa: PLR0912, C901 will not raise an error. But calling an "not_existing_attribute" will raise an error. If any bus name from bus_from_to_names is not found in the branch type. """ + station_grid_model_id = str(station_buses["grid_model_id"].values[0]) + if branch_types is None: branch_types = [ "line", @@ -297,6 +300,8 @@ def get_branches_from_station( # noqa: PLR0912, C901 # asset is not directly connected to a busbar -> get connection path asset_connection = get_asset_connection_path_to_busbars( network=network, + station_grid_model_id=station_grid_model_id, + asset_grid_model_id=str(branch[foreign_key]), asset_bus=asset_bus, station_buses=station_buses, save_col_name=foreign_key, @@ -449,6 +454,11 @@ def get_station_from_id( switchable_assets_list = get_list_of_switchable_assets_from_df( station_branches=station_branches, asset_bay_list=asset_connection_path ) + asset_terminals = ( + station_branches["branch_end"].tolist() + if "branch_end" in station_branches.columns + else [None] * len(station_branches) + ) voltage_level_float = get_parameter_from_station(network=network, station_bus_index=station_id_list, parameter="vn_kv") # region = get_parameter_from_station(network, station_name, "zone") @@ -467,6 +477,8 @@ def get_station_from_id( busbars=busbar_list, couplers=coupler_list, assets=switchable_assets_list, + asset_terminals=asset_terminals, + asset_bays=asset_connection_path, asset_switching_table=switching_matrix, ) @@ -609,6 +621,8 @@ def get_station_bus_df( # TODO: replace by networkX_logic_modules def get_asset_connection_path_to_busbars( # noqa: PLR0915 network: pp.pandapowerNet, + station_grid_model_id: str, + asset_grid_model_id: str, asset_bus: int, station_buses: pd.DataFrame, save_col_name: str = "equipment", @@ -619,6 +633,10 @@ def get_asset_connection_path_to_busbars( # noqa: PLR0915 ---------- network: pp.pandapowerNet pandapower network object + station_grid_model_id: str + Station identifier owning the asset bay. + asset_grid_model_id: str + Asset identifier for which the asset bay is constructed. asset_bus: int Asset bus id for which the connection path should be retrieved. station_buses: pd.DataFrame @@ -722,6 +740,7 @@ def get_asset_connection_path_to_busbars( # noqa: PLR0915 sl_switch_grid_model_id = None asset_connection = AssetBay( + asset_bay_id=build_asset_bay_id(station_grid_model_id, asset_grid_model_id), sl_switch_grid_model_id=sl_switch_grid_model_id, dv_switch_grid_model_id=circuit_breaker[save_col_name].iloc[0], sr_switch_grid_model_id=final_buses, diff --git a/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py b/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py index e62db06e9..fbdfe6960 100644 --- a/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py +++ b/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py @@ -31,7 +31,7 @@ ) from toop_engine_importer.network_graph.network_graph_data import add_graph_specific_data from toop_engine_importer.network_graph.powsybl_station_to_graph import get_station -from toop_engine_interfaces.asset_topology import AssetBay +from toop_engine_interfaces.asset_topology import AssetBay, build_asset_bay_id def test_remove_double_connections(): @@ -295,26 +295,31 @@ def test_asset_bay(network_graph_for_asset_topoV2_S3: tuple[nx.Graph, NetworkGra ) expected = { "L3": AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "L3"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="L32_BREAKER", sr_switch_grid_model_id={"BBS3_1": "L32_DISCONNECTOR_5_0", "BBS3_2": "L32_DISCONNECTOR_5_1"}, ), "L6": AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "L6"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="L62_BREAKER", sr_switch_grid_model_id={"BBS3_1": "L62_DISCONNECTOR_7_0", "BBS3_2": "L62_DISCONNECTOR_7_1"}, ), "L7": AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "L7"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="L72_BREAKER", sr_switch_grid_model_id={"BBS3_1": "L72_DISCONNECTOR_9_0", "BBS3_2": "L72_DISCONNECTOR_9_1"}, ), "L9": AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "L9"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="L91_BREAKER", sr_switch_grid_model_id={"BBS3_1": "L91_DISCONNECTOR_11_0", "BBS3_2": "L91_DISCONNECTOR_11_1"}, ), "load2": AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "load2"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="load2_BREAKER", sr_switch_grid_model_id={"BBS3_1": "load2_DISCONNECTOR_19_0", "BBS3_2": "load2_DISCONNECTOR_19_1"}, @@ -325,6 +330,7 @@ def test_asset_bay(network_graph_for_asset_topoV2_S3: tuple[nx.Graph, NetworkGra for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list(): asset_bay, logs = get_asset_bay( network_graph_data.switches, + station_grid_model_id=substation_id, asset_grid_model_id=asset_grid_model_id, busbar_df=busbar_df, edge_connection_info=edge_connection_info, @@ -338,6 +344,7 @@ def test_asset_bay(network_graph_for_asset_topoV2_S3: tuple[nx.Graph, NetworkGra with pytest.raises(ValueError, match="Expected 3 switches, but got"): get_asset_bay( switches_df=switches_df, + station_grid_model_id=substation_id, asset_grid_model_id="L3", busbar_df=busbar_df, edge_connection_info=edge_connection_info, @@ -348,11 +355,13 @@ def test_asset_bay(network_graph_for_asset_topoV2_S3: tuple[nx.Graph, NetworkGra asset_grid_model_id, logs = get_asset_bay( switches_df=switches_df, + station_grid_model_id=substation_id, asset_grid_model_id="L3", busbar_df=busbar_df, edge_connection_info=edge_connection_info, ) expected = AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "L3"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="L32_BREAKER", sr_switch_grid_model_id={"BBS3_1": "L32_DISCONNECTOR_5_0", "BBS3_2": "L32_DISCONNECTOR_5_1"}, @@ -366,6 +375,7 @@ def test_asset_bay(network_graph_for_asset_topoV2_S3: tuple[nx.Graph, NetworkGra switches_df.drop(2, inplace=True) asset_grid_model_id, logs = get_asset_bay( switches_df=switches_df, + station_grid_model_id=substation_id, asset_grid_model_id="L3", busbar_df=busbar_df, edge_connection_info=edge_connection_info, @@ -376,11 +386,13 @@ def test_asset_bay(network_graph_for_asset_topoV2_S3: tuple[nx.Graph, NetworkGra edge_connection_info["L62_DISCONNECTOR_7_0"].direct_busbar_grid_model_id = "" asset_grid_model_id, logs = get_asset_bay( switches_df=switches_df, + station_grid_model_id=substation_id, asset_grid_model_id="L6", busbar_df=busbar_df, edge_connection_info=edge_connection_info, ) expected = AssetBay( + asset_bay_id=build_asset_bay_id(substation_id, "L6"), sl_switch_grid_model_id="L62_DISCONNECTOR_7_0", dv_switch_grid_model_id="L62_BREAKER", sr_switch_grid_model_id={"BBS3_2": "L62_DISCONNECTOR_7_1"}, 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 28178c1ba..7be95e40c 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 @@ -160,15 +160,16 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert couplers[0].in_service assets = res.assets + asset_bays = res.asset_bays assert len(assets) == 5 assert assets[0].grid_model_id == "L3" assert assets[0].type == "LINE" assert assets[0].name == "" assert assets[0].in_service assert assets[0].branch_end is None - assert assets[0].asset_bay.sl_switch_grid_model_id is None - assert assets[0].asset_bay.dv_switch_grid_model_id == "L32_BREAKER" - assert assets[0].asset_bay.sr_switch_grid_model_id == { + assert asset_bays[0].sl_switch_grid_model_id is None + assert asset_bays[0].dv_switch_grid_model_id == "L32_BREAKER" + assert asset_bays[0].sr_switch_grid_model_id == { "BBS3_1": "L32_DISCONNECTOR_3_0", "BBS3_2": "L32_DISCONNECTOR_3_1", } @@ -178,9 +179,9 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[1].name == "" assert assets[1].in_service assert assets[1].branch_end is None - assert assets[1].asset_bay.sl_switch_grid_model_id is None - assert assets[1].asset_bay.dv_switch_grid_model_id == "L62_BREAKER" - assert assets[1].asset_bay.sr_switch_grid_model_id == { + assert asset_bays[1].sl_switch_grid_model_id is None + assert asset_bays[1].dv_switch_grid_model_id == "L62_BREAKER" + assert asset_bays[1].sr_switch_grid_model_id == { "BBS3_1": "L62_DISCONNECTOR_5_0", "BBS3_2": "L62_DISCONNECTOR_5_1", } @@ -190,9 +191,9 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[2].name == "" assert assets[2].in_service assert assets[2].branch_end is None - assert assets[2].asset_bay.sl_switch_grid_model_id is None - assert assets[2].asset_bay.dv_switch_grid_model_id == "L72_BREAKER" - assert assets[2].asset_bay.sr_switch_grid_model_id == { + assert asset_bays[2].sl_switch_grid_model_id is None + assert asset_bays[2].dv_switch_grid_model_id == "L72_BREAKER" + assert asset_bays[2].sr_switch_grid_model_id == { "BBS3_1": "L72_DISCONNECTOR_7_0", "BBS3_2": "L72_DISCONNECTOR_7_1", } @@ -202,9 +203,9 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[3].name == "" assert assets[3].in_service assert assets[3].branch_end is None - assert assets[3].asset_bay.sl_switch_grid_model_id is None - assert assets[3].asset_bay.dv_switch_grid_model_id == "L91_BREAKER" - assert assets[3].asset_bay.sr_switch_grid_model_id == { + assert asset_bays[3].sl_switch_grid_model_id is None + assert asset_bays[3].dv_switch_grid_model_id == "L91_BREAKER" + assert asset_bays[3].sr_switch_grid_model_id == { "BBS3_1": "L91_DISCONNECTOR_9_0", "BBS3_2": "L91_DISCONNECTOR_9_1", } @@ -214,9 +215,9 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[4].name == "" assert assets[4].in_service assert assets[4].branch_end is None - assert assets[4].asset_bay.sl_switch_grid_model_id is None - assert assets[4].asset_bay.dv_switch_grid_model_id == "load2_BREAKER" - assert assets[4].asset_bay.sr_switch_grid_model_id == { + assert asset_bays[4].sl_switch_grid_model_id is None + assert asset_bays[4].dv_switch_grid_model_id == "load2_BREAKER" + assert asset_bays[4].sr_switch_grid_model_id == { "BBS3_1": "load2_DISCONNECTOR_13_0", "BBS3_2": "load2_DISCONNECTOR_13_1", } @@ -237,8 +238,11 @@ def test_get_station_edge_cases_one_bay_two_assets(asset_topo_edge_cases_node_br station_info = SubstationInformation(**station_info) res = get_station(net, "VL1_1", station_info) load_assets = [asset for asset in res.assets if "load" in asset.grid_model_id] + load_asset_bays = [ + asset_bay for asset, asset_bay in zip(res.assets, res.asset_bays, strict=True) if "load" in asset.grid_model_id + ] assert len(load_assets) == 2, "Expected two loads" - assert load_assets[0].asset_bay == load_assets[1].asset_bay, "Both loads should be in the same asset bay" + assert load_asset_bays[0] == load_asset_bays[1], "Both loads should be in the same asset bay" def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): @@ -269,10 +273,10 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): assert res.asset_switching_table.shape == (len(res.busbars), len(res.assets)) assert res.asset_connectivity.shape == (len(res.busbars), len(res.assets)) - for asset in res.assets: - if asset.asset_bay is None: + for asset_bay in res.asset_bays: + if asset_bay is None: continue - assert set(asset.asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids + assert set(asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids station_info = {"name": "Station_ID", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL2"} station_info = SubstationInformation(**station_info) @@ -307,10 +311,10 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): assert res.asset_connectivity.shape == (len(res.busbars), len(res.assets)) assert res.asset_connectivity.any(axis=0).all() - for asset in res.assets: - if asset.asset_bay is None: + for asset_bay in res.asset_bays: + if asset_bay is None: continue - assert set(asset.asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids + assert set(asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids def test_get_topo_integration(basic_node_breaker_network_powsybl_grid: Network): diff --git a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py index 713e84d99..437182061 100644 --- a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py +++ b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py @@ -16,6 +16,7 @@ AssetBay, MaterializedStation, Topology, + build_asset_bay_id, ) @@ -121,26 +122,31 @@ def test_get_branches_from_station(pp_network_w_switches): expected_switching_matrix = np.array([[True, True, True, True, True]]) expected_asset_connection = [ AssetBay( + asset_bay_id=build_asset_bay_id("16%%bus", "HV Line1"), sl_switch_grid_model_id="SB DS2.1", dv_switch_grid_model_id="SB CB2", sr_switch_grid_model_id={"16%%bus": "SB DS2.2"}, ), AssetBay( + asset_bay_id=build_asset_bay_id("16%%bus", "HV Line6"), sl_switch_grid_model_id="SB DS3.1", dv_switch_grid_model_id="SB CB3", sr_switch_grid_model_id={"16%%bus": "SB DS3.2"}, ), AssetBay( + asset_bay_id=build_asset_bay_id("16%%bus", "EHV-HV-Trafo"), sl_switch_grid_model_id="SB DS1.1", dv_switch_grid_model_id="SB CB1", sr_switch_grid_model_id={"16%%bus": "SB DS1.2"}, ), AssetBay( + asset_bay_id=build_asset_bay_id("16%%bus", "MV Net 0"), sl_switch_grid_model_id="SB DS4.1", dv_switch_grid_model_id="SB CB4", sr_switch_grid_model_id={"16%%bus": "SB DS4.2"}, ), AssetBay( + asset_bay_id=build_asset_bay_id("16%%bus", "Wind Park"), sl_switch_grid_model_id="SB DS5.1", dv_switch_grid_model_id="SB CB5", sr_switch_grid_model_id={"16%%bus": "SB DS5.2"}, @@ -222,6 +228,7 @@ def test_get_branches_from_station_edge_cases(pp_network_w_switches): net.switch.loc[31, "element"] = 19 net.switch.loc[33, "closed"] = False expected_path = AssetBay( + asset_bay_id=build_asset_bay_id("16%%bus", "HV Line6"), sl_switch_grid_model_id=None, dv_switch_grid_model_id="SB CB3", sr_switch_grid_model_id={"16%%bus": "SB DS3.2"}, @@ -243,6 +250,7 @@ def test_get_branches_from_station_edge_cases(pp_network_w_switches): net.switch.loc[5, "closed"] = True expected_path = [ AssetBay( + asset_bay_id=build_asset_bay_id("0%%bus", "EHV-HV-Trafo"), sl_switch_grid_model_id="DB DS11", dv_switch_grid_model_id="DB CB2", sr_switch_grid_model_id={"1%%bus": "DB DS4", "0%%bus": "DB DS5"}, diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index 4f0005ed5..fbccc6ae7 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -72,7 +72,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow ], couplers=[], asset_ids=["asset0"], - asset_branch_ends=[None], + asset_terminals=[None], asset_bay_ids=[None], asset_switching_table=np.array([[True], [False]]), ) @@ -82,7 +82,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow busbars=[Busbar(int_id=0, grid_model_id="bb2", bus_branch_bus_id=buses.index[1])], couplers=[], asset_ids=["asset1"], - asset_branch_ends=[None], + asset_terminals=[None], asset_bay_ids=[None], asset_switching_table=np.array([[True]]), ) diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py index 493814925..08fb6f4f6 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py @@ -12,7 +12,7 @@ import numpy as np from beartype.typing import Any, Literal, Optional, TypeAlias, Union, get_args -from numpydantic import NDArray, Shape +from jaxtyping import ArrayLike, Bool from pydantic import BaseModel, Field, field_validator, model_validator @@ -133,7 +133,7 @@ class AssetBay(BaseModel): """ - asset_bay_id: Optional[str] = None + asset_bay_id: str """Topology-scoped identifier for the asset bay.""" sl_switch_grid_model_id: Optional[str] = None @@ -253,19 +253,6 @@ class SwitchableAsset(BaseModel): ignored. This shall not be used for elements intentionally disconnected, instead set all zeros in the switching table.""" - branch_end: Optional[BranchEnd] = None - """If the asset was a branch, this can store which end of the branch was connected to the - station in the original grid model. This can take the values "from", "to", "hv", "mv", "lv", - where from/to works for lines and hv/mv/lv works for transformers. This should only be set if - this is needed for the postprocessing, in theory a branch should be identifiable by the branch - id and the station id. Injection-type assets like generators and loads should not have this set. - """ - - asset_bay: Optional[AssetBay] = None - """ The asset bay (Schaltfeld) of the asset. - The connection path is used to determine the physical connection of the asset to the busbar. - None of these switches will be found in the network model, they are only used for the asset topology.""" - asset_bay_id: Optional[str] = None """Topology-scoped identifier for the asset bay associated with this asset.""" @@ -400,6 +387,11 @@ class _StationStructure(BaseModel): """The unique identifier of the station. Expects the bus-branch model bus_id, not the full voltage level id. + + Included are all assets, busbars and couplers that are connectable via switches. + Buses in the same station that are connected via branches are excluded in this specific bus. + + This means, that two stations/buses can have the same elements if the station is currently split. """ name: Optional[str] = None @@ -420,7 +412,7 @@ class _StationStructure(BaseModel): couplers: list[BusbarCoupler] """The list of couplers at the station.""" - asset_switching_table: NDArray[Shape[" * bus, * asset"], bool] + asset_switching_table: Bool[ArrayLike, "n_bus n_asset"] """Holds the switching of each asset to each busbar, shape (n_bus, n_asset). An entry is true if the asset is connected to the busbar. @@ -428,7 +420,7 @@ class _StationStructure(BaseModel): to be present between these busbars. """ - asset_connectivity: Optional[NDArray[Shape[" * bus, * asset"], bool]] = None + asset_connectivity: Optional[Bool[ArrayLike, "n_bus n_asset"]] = None """Holds the all possible layouts of the asset_switching_table, shape (n_bus, n_asset). An entry is true if it is possible to connect an asset to the busbar. @@ -523,9 +515,48 @@ class MaterializedStation(_StationStructure): stored on Topology.assets instead. """ + asset_terminals: list[Optional[BranchEnd]] = Field(default_factory=list) + """Station-local branch terminals aligned with ``assets``. + + Each entry describes which terminal of the asset is connected in this station view. + """ + + asset_bays: list[Optional[AssetBay]] = Field(default_factory=list) + """Station-local asset bay payloads aligned with ``assets``. + + Each entry corresponds to the asset at the same index. ``None`` means the asset has no bay. + """ + @model_validator(mode="after") def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": """Check if switching-table-aligned station-local assets match the matrix shapes.""" + if len(self.asset_terminals) == 0: + self.asset_terminals = [None] * len(self.assets) + elif len(self.asset_terminals) != len(self.assets): + raise ValueError( + f"asset_terminals length {len(self.asset_terminals)} does not match assets length {len(self.assets)}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + + if len(self.asset_bays) == 0: + self.asset_bays = [None] * len(self.assets) + elif len(self.asset_bays) != len(self.assets): + raise ValueError( + f"asset_bays length {len(self.asset_bays)} does not match assets length {len(self.assets)}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + + for index, (asset, asset_bay) in enumerate(zip(self.assets, self.asset_bays, strict=True)): + if asset_bay is None: + continue + if asset.asset_bay_id is None: + asset.asset_bay_id = asset_bay.asset_bay_id + elif asset.asset_bay_id != asset_bay.asset_bay_id: + raise ValueError( + f"asset_bay_id mismatch for asset index {index}: {asset.asset_bay_id} != {asset_bay.asset_bay_id}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + _validate_station_switching_tables( station_grid_model_id=self.grid_model_id, station_name=self.name, @@ -546,9 +577,9 @@ def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": """Check if the asset bay bus is in busbars.""" busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] - for asset in self.assets: - if asset.asset_bay is not None: - for busbar_id in asset.asset_bay.sr_switch_grid_model_id.keys(): + for asset, asset_bay in zip(self.assets, self.asset_bays, strict=True): + if asset_bay is not None: + for busbar_id in asset_bay.sr_switch_grid_model_id.keys(): if busbar_id not in busbar_grid_model_id: raise ValueError( f"busbar_id {busbar_id} in asset {asset.grid_model_id} does not exist in busbars" @@ -578,6 +609,8 @@ def __eq__(self, other: object) -> bool: and self.busbars == other.busbars and self.couplers == other.couplers and self.assets == other.assets + and self.asset_terminals == other.asset_terminals + and self.asset_bays == other.asset_bays and np.array_equal(self.asset_switching_table, other.asset_switching_table) and ( np.array_equal(self.asset_connectivity, other.asset_connectivity) @@ -601,8 +634,8 @@ class RawStation(_StationStructure): These are station-local references into Topology.assets. """ - asset_branch_ends: list[Optional[BranchEnd]] - """Station-local branch ends aligned with the switching tables.""" + asset_terminals: list[Optional[BranchEnd]] + """Station-local terminals aligned with the switching tables.""" asset_bay_ids: list[Optional[str]] """Topology-scoped asset bay identifiers aligned with the switching tables.""" @@ -610,9 +643,9 @@ class RawStation(_StationStructure): @model_validator(mode="after") def check_asset_reference_alignment(self: "RawStation") -> "RawStation": """Check if station-local asset reference arrays are aligned.""" - if len(self.asset_branch_ends) != len(self.asset_ids): + if len(self.asset_terminals) != len(self.asset_ids): raise ValueError( - f"asset_branch_ends length {len(self.asset_branch_ends)} " + f"asset_terminals length {len(self.asset_terminals)} " f"does not match asset_ids length {len(self.asset_ids)}" f" Station_id: {self.grid_model_id}, Name: {self.name}" ) @@ -650,7 +683,7 @@ def __eq__(self, other: object) -> bool: and self.busbars == other.busbars and self.couplers == other.couplers and self.asset_ids == other.asset_ids - and self.asset_branch_ends == other.asset_branch_ends + and self.asset_terminals == other.asset_terminals and self.asset_bay_ids == other.asset_bay_ids and np.array_equal(self.asset_switching_table, other.asset_switching_table) and ( @@ -758,22 +791,24 @@ def materialize_stations(self) -> list[MaterializedStation]: for station in self.raw_stations: station_assets: list[SwitchableAsset] = [] - for asset_id, asset_branch_end, asset_bay_id in zip( - station.asset_ids, station.asset_branch_ends, station.asset_bay_ids, strict=True + for asset_id, _asset_terminal, asset_bay_id in zip( + station.asset_ids, station.asset_terminals, station.asset_bay_ids, strict=True ): asset = asset_map[asset_id] - asset_bay = asset_bay_map.get(asset_bay_id) if asset_bay_id is not None else None station_assets.append( asset.model_copy( update={ - "branch_end": asset_branch_end, "asset_bay_id": asset_bay_id, - "asset_bay": asset_bay.model_copy(deep=True) if asset_bay is not None else None, }, deep=True, ) ) + station_asset_bays = [ + asset_bay_map[asset_bay_id].model_copy(deep=True) if asset_bay_id is not None else None + for asset_bay_id in station.asset_bay_ids + ] + materialized_stations.append( MaterializedStation( grid_model_id=station.grid_model_id, @@ -784,6 +819,8 @@ def materialize_stations(self) -> list[MaterializedStation]: busbars=station.busbars, couplers=station.couplers, assets=station_assets, + asset_terminals=station.asset_terminals, + asset_bays=station_asset_bays, asset_switching_table=station.asset_switching_table, asset_connectivity=station.asset_connectivity, model_log=station.model_log, @@ -792,8 +829,47 @@ def materialize_stations(self) -> list[MaterializedStation]: return materialized_stations + def get_asset_bay_ids_for_asset(self, asset_grid_model_id: str) -> list[str]: + """Return all station-scoped asset bay ids connected to a topology asset. + + Parameters + ---------- + asset_grid_model_id : str + Grid model id of the topology-owned asset. + + Returns + ------- + list[str] + Ordered unique asset bay ids connected to the asset across all raw stations. + """ + asset_bay_ids: list[str] = [] + seen_ids: set[str] = set() + for station in self.raw_stations: + for asset_id, asset_bay_id in zip(station.asset_ids, station.asset_bay_ids, strict=True): + if asset_id != asset_grid_model_id or asset_bay_id is None or asset_bay_id in seen_ids: + continue + seen_ids.add(asset_bay_id) + asset_bay_ids.append(asset_bay_id) + return asset_bay_ids + + def get_asset_bays_for_asset(self, asset_grid_model_id: str) -> list[AssetBay]: + """Return all station-scoped asset bays connected to a topology asset. -def _default_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, occurrence_index: int) -> str: + Parameters + ---------- + asset_grid_model_id : str + Grid model id of the topology-owned asset. + + Returns + ------- + list[AssetBay] + Ordered unique asset bay payloads connected to the asset across all raw stations. + """ + asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} + return [asset_bay_map[asset_bay_id] for asset_bay_id in self.get_asset_bay_ids_for_asset(asset_grid_model_id)] + + +def build_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, occurrence_index: int = 0) -> str: """Create a deterministic station-scoped asset bay identifier. Parameters @@ -802,7 +878,7 @@ def _default_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, Station identifier owning the asset bay. asset_grid_model_id : str Asset identifier for which the bay id is created. - occurrence_index : int + occurrence_index : int, default=0 Zero-based occurrence index for repeated asset ids within one station. Returns @@ -834,35 +910,25 @@ def topology_parts_from_materialized_station( assets: list[SwitchableAsset] = [] asset_bays: list[AssetBay] = [] asset_ids: list[str] = [] - asset_branch_ends: list[Optional[BranchEnd]] = [] + asset_terminals = list(station.asset_terminals) asset_bay_ids: list[Optional[str]] = [] - station_asset_occurrences: dict[str, int] = {} - - for asset in station.assets: + for asset, asset_bay in zip(station.assets, station.asset_bays, strict=True): asset_id = asset.grid_model_id asset_bay_id: Optional[str] = asset.asset_bay_id - asset_occurrence_index = station_asset_occurrences.get(asset_id, 0) - station_asset_occurrences[asset_id] = asset_occurrence_index + 1 - if asset.asset_bay is not None: - asset_bay = asset.asset_bay - asset_bay_id = asset_bay.asset_bay_id or _default_asset_bay_id( - station.grid_model_id, asset_id, asset_occurrence_index - ) - asset_bays.append(asset_bay.model_copy(update={"asset_bay_id": asset_bay_id}, deep=True)) + if asset_bay is not None: + asset_bay_id = asset_bay.asset_bay_id + asset_bays.append(asset_bay.model_copy(deep=True)) assets.append( asset.model_copy( update={ - "branch_end": None, "asset_bay_id": None, - "asset_bay": None, }, deep=True, ) ) asset_ids.append(asset_id) - asset_branch_ends.append(asset.branch_end) asset_bay_ids.append(asset_bay_id) return ( @@ -875,7 +941,7 @@ def topology_parts_from_materialized_station( busbars=station.busbars, couplers=station.couplers, asset_ids=asset_ids, - asset_branch_ends=asset_branch_ends, + asset_terminals=asset_terminals, asset_bay_ids=asset_bay_ids, asset_switching_table=station.asset_switching_table, asset_connectivity=station.asset_connectivity, @@ -915,8 +981,6 @@ def topology_from_materialized_stations(reference_topology: Topology, stations: elif existing_asset != asset: raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") for asset_bay in station_asset_bays: - if asset_bay.asset_bay_id is None: - continue existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) if existing_asset_bay is None: topology_asset_bays[asset_bay.asset_bay_id] = asset_bay diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py index db01ecf50..999246028 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py @@ -18,6 +18,7 @@ from fsspec.implementations.local import LocalFileSystem from toop_engine_interfaces.asset_topology import ( AppliedStation, + AssetBay, Busbar, BusbarCoupler, MaterializedStation, @@ -129,32 +130,28 @@ def remove_busbar(station: MaterializedStation, grid_model_id: str) -> Materiali np.delete(station.asset_connectivity, index, axis=0) if station.asset_connectivity is not None else None ) - def filter_sr_keys(asset: SwitchableAsset) -> SwitchableAsset: - """Filter out the sr keys from the asset bay""" - if asset.asset_bay is None: - return asset - return asset.model_copy( + def filter_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: + """Filter out the sr keys from the asset bay.""" + if asset_bay is None: + return None + return asset_bay.model_copy( update={ - "asset_bay": asset.asset_bay.model_copy( - update={ - "sr_switch_grid_model_id": { - busbar_id: foreign_id - for busbar_id, foreign_id in asset.asset_bay.sr_switch_grid_model_id.items() - if busbar_id != grid_model_id - } - } - ) + "sr_switch_grid_model_id": { + busbar_id: foreign_id + for busbar_id, foreign_id in asset_bay.sr_switch_grid_model_id.items() + if busbar_id != grid_model_id + } } ) - assets = [filter_sr_keys(a) for a in station.assets] + asset_bays = [filter_sr_keys(asset_bay) for asset_bay in station.asset_bays] # Create a new station object with the modified busbars, couplers, and asset switching table new_station = station.model_copy( update={ "busbars": busbars, "couplers": couplers, - "assets": assets, + "asset_bays": asset_bays, "asset_switching_table": asset_switching_table, "asset_connectivity": asset_connectivity, } @@ -183,6 +180,14 @@ def filter_out_of_service_assets(station: MaterializedStation) -> MaterializedSt return station.model_copy( update={ "assets": [asset for asset in station.assets if asset.in_service], + "asset_terminals": [ + branch_end + for branch_end, in_service in zip(station.asset_terminals, in_service_assets, strict=True) + if in_service + ], + "asset_bays": [ + asset_bay for asset_bay, in_service in zip(station.asset_bays, in_service_assets, strict=True) if in_service + ], "asset_switching_table": station.asset_switching_table[:, in_service_assets], "asset_connectivity": station.asset_connectivity[:, in_service_assets] if station.asset_connectivity is not None @@ -449,6 +454,10 @@ def filter_assets_by_type( new_station = station.model_copy( update={ "assets": kept_assets, + "asset_terminals": [ + branch_end for branch_end, mask in zip(station.asset_terminals, asset_mask, strict=True) if mask + ], + "asset_bays": [asset_bay for asset_bay, mask in zip(station.asset_bays, asset_mask, strict=True) if mask], "asset_switching_table": station.asset_switching_table[:, asset_mask], "asset_connectivity": station.asset_connectivity[:, asset_mask] if station.asset_connectivity is not None @@ -1275,6 +1284,8 @@ def order_station_assets( station = station.model_copy( update={ "assets": new_assets, + "asset_terminals": [station.asset_terminals[index] for index in old_positions], + "asset_bays": [station.asset_bays[index] for index in old_positions], "asset_switching_table": asset_switching_table, "asset_connectivity": asset_connectivity, } @@ -1389,30 +1400,28 @@ def fuse_coupler( busbar_to_remove = station.busbars[busbar_index_to_remove] busbar_to_keep = station.busbars[busbar_index_to_keep] - def _replace_sr_keys(asset: SwitchableAsset) -> SwitchableAsset: - """Update the sr switch asset if it is connected to the removed busbar.""" - if asset.asset_bay is None: - return asset + def _replace_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: + """Update the sr switch asset bay if it is connected to the removed busbar.""" + if asset_bay is None: + return None if ( - busbar_to_remove.grid_model_id in asset.asset_bay.sr_switch_grid_model_id.keys() - and busbar_to_keep.grid_model_id in asset.asset_bay.sr_switch_grid_model_id.keys() + busbar_to_remove.grid_model_id in asset_bay.sr_switch_grid_model_id.keys() + and busbar_to_keep.grid_model_id in asset_bay.sr_switch_grid_model_id.keys() ): # If both busbars are present, we need to remove the one that is not kept new_sr_switch_grid_model_id = { key: foreign_id - for (key, foreign_id) in asset.asset_bay.sr_switch_grid_model_id.items() + for (key, foreign_id) in asset_bay.sr_switch_grid_model_id.items() if key != busbar_to_remove.grid_model_id } else: # If the target busbar is not present, we change the key new_sr_switch_grid_model_id = { (busbar_to_keep.grid_model_id if key == busbar_to_remove.grid_model_id else key): foreign_id - for (key, foreign_id) in asset.asset_bay.sr_switch_grid_model_id.items() + for (key, foreign_id) in asset_bay.sr_switch_grid_model_id.items() } - return asset.model_copy( - update={"asset_bay": asset.asset_bay.model_copy(update={"sr_switch_grid_model_id": new_sr_switch_grid_model_id})} - ) + return asset_bay.model_copy(update={"sr_switch_grid_model_id": new_sr_switch_grid_model_id}) def _replace_int_id(coupler: BusbarCoupler) -> BusbarCoupler: """Update coupler int-ids that are pointing to the removed busbar.""" @@ -1424,14 +1433,14 @@ def _replace_int_id(coupler: BusbarCoupler) -> BusbarCoupler: new_busbars = [b for i, b in enumerate(station.busbars) if i != busbar_index_to_remove] new_couplers = [_replace_int_id(c) for c in station.couplers if c.grid_model_id != coupler_grid_model_id] - new_assets = [_replace_sr_keys(a) for a in station.assets] + new_asset_bays = [_replace_sr_keys(asset_bay) for asset_bay in station.asset_bays] station = station.model_copy( update={ "busbars": new_busbars, "asset_switching_table": new_switching_table, "asset_connectivity": new_connectivity_table, - "assets": new_assets, + "asset_bays": new_asset_bays, "couplers": new_couplers, } ) diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index 335463718..4e1bc109a 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -20,6 +20,7 @@ RawStation, SwitchableAsset, Topology, + build_asset_bay_id, topology_from_materialized_stations, ) from toop_engine_interfaces.asset_topology_helpers import ( @@ -333,7 +334,7 @@ def test_topology_station_is_split() -> None: ], couplers=[], asset_ids=["line1"], - asset_branch_ends=[None], + asset_terminals=[None], asset_bay_ids=[None], asset_switching_table=np.array([[True], [False]]), ) @@ -459,14 +460,18 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: SwitchableAsset( grid_model_id="line1", branch_end="from", - asset_bay=AssetBay( - asset_bay_id="station1::line1::bay", - dv_switch_grid_model_id="dv1", - sr_switch_grid_model_id={"busbar1": "sr1"}, - ), + asset_bay_id="station1::line1::bay", ), SwitchableAsset(grid_model_id="load1"), ], + asset_bays=[ + AssetBay( + asset_bay_id="station1::line1::bay", + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ), + None, + ], asset_switching_table=np.array([[True, False], [False, True]]), grid_model_id="station1", ) @@ -486,7 +491,7 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: busbars=station.busbars, couplers=station.couplers, asset_ids=["line1", "load1"], - asset_branch_ends=["from", None], + asset_terminals=["from", None], asset_bay_ids=["station1::line1::bay", None], asset_switching_table=np.array([[True, False], [False, True]]), ) @@ -506,14 +511,15 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_two_station_views() -> None: - asset_from = SwitchableAsset(grid_model_id="line1", type="line", branch_end="from") - asset_to = SwitchableAsset(grid_model_id="line1", type="line", branch_end="to") + asset_from = SwitchableAsset(grid_model_id="line1", type="line") + asset_to = SwitchableAsset(grid_model_id="line1", type="line") station_from = MaterializedStation( grid_model_id="station_from", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], assets=[asset_from], + asset_terminals=["from"], asset_switching_table=np.array([[True]]), ) station_to = MaterializedStation( @@ -521,6 +527,7 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw busbars=[Busbar(int_id=1, grid_model_id="busbar2")], couplers=[], assets=[asset_to], + asset_terminals=["to"], asset_switching_table=np.array([[True]]), ) @@ -535,9 +542,9 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw assert [asset.grid_model_id for asset in topology.assets] == ["line1"] assert topology.raw_stations[0].asset_ids == ["line1"] - assert topology.raw_stations[0].asset_branch_ends == ["from"] + assert topology.raw_stations[0].asset_terminals == ["from"] assert topology.raw_stations[1].asset_ids == ["line1"] - assert topology.raw_stations[1].asset_branch_ends == ["to"] + assert topology.raw_stations[1].asset_terminals == ["to"] assert topology.materialize_stations() == [station_from, station_to] @@ -550,8 +557,15 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ SwitchableAsset( grid_model_id="line1", type="line", - branch_end="from", - asset_bay=AssetBay(dv_switch_grid_model_id="dv_from", sr_switch_grid_model_id={"busbar1": "sr_from"}), + asset_bay_id=build_asset_bay_id("station_from", "line1"), + ) + ], + asset_terminals=["from"], + asset_bays=[ + AssetBay( + asset_bay_id=build_asset_bay_id("station_from", "line1"), + dv_switch_grid_model_id="dv_from", + sr_switch_grid_model_id={"busbar1": "sr_from"}, ) ], asset_switching_table=np.array([[True]]), @@ -564,8 +578,15 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ SwitchableAsset( grid_model_id="line1", type="line", - branch_end="to", - asset_bay=AssetBay(dv_switch_grid_model_id="dv_to", sr_switch_grid_model_id={"busbar2": "sr_to"}), + asset_bay_id=build_asset_bay_id("station_to", "line1"), + ) + ], + asset_terminals=["to"], + asset_bays=[ + AssetBay( + asset_bay_id=build_asset_bay_id("station_to", "line1"), + dv_switch_grid_model_id="dv_to", + sr_switch_grid_model_id={"busbar2": "sr_to"}, ) ], asset_switching_table=np.array([[True]]), @@ -597,12 +618,24 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ SwitchableAsset( grid_model_id="line1", type="line", - asset_bay=AssetBay(dv_switch_grid_model_id="dv1", sr_switch_grid_model_id={"busbar1": "sr1"}), + asset_bay_id=build_asset_bay_id("station1", "line1"), ), SwitchableAsset( grid_model_id="line1", type="line", - asset_bay=AssetBay(dv_switch_grid_model_id="dv2", sr_switch_grid_model_id={"busbar1": "sr2"}), + asset_bay_id=build_asset_bay_id("station1", "line1", 1), + ), + ], + asset_bays=[ + AssetBay( + asset_bay_id=build_asset_bay_id("station1", "line1"), + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ), + AssetBay( + asset_bay_id=build_asset_bay_id("station1", "line1", 1), + dv_switch_grid_model_id="dv2", + sr_switch_grid_model_id={"busbar1": "sr2"}, ), ], asset_switching_table=np.array([[True, True]]), @@ -924,6 +957,7 @@ def test_filter_assets_by_type() -> None: def test_asset_bay() -> None: # Test valid AssetBay path = AssetBay( + asset_bay_id="station1::line1::bay", sl_switch_grid_model_id="sl_switch_1", dv_switch_grid_model_id="dv_switch_1", sr_switch_grid_model_id={"busbar1": "sr_switch_1", "busbar2": "sr_switch_2"}, @@ -931,6 +965,7 @@ def test_asset_bay() -> None: assert path is not None path = AssetBay( + asset_bay_id="station1::line2::bay", sl_switch_grid_model_id="sl_switch_1", dv_switch_grid_model_id="dv_switch_1", sr_switch_grid_model_id={"busbar1": "sr_switch_1", "busbar2": "sr_switch_2"}, @@ -941,6 +976,7 @@ def test_asset_bay() -> None: # Test AssetBay with missing dv_switch_grid_model_id with pytest.raises(ValidationError, match="Field required"): path = AssetBay( + asset_bay_id="station1::line3::bay", sl_switch_grid_model_id="sl_switch_1", sr_switch_grid_model_id={"busbar1": "sr_switch_1", "busbar2": "sr_switch_2"}, ) @@ -948,6 +984,7 @@ def test_asset_bay() -> None: # Test AssetBay with empty sr_switch_grid_model_id with pytest.raises(ValidationError, match="sr_switch_grid_model_id must not be empty"): path = AssetBay( + asset_bay_id="station1::line4::bay", sl_switch_grid_model_id="sl_switch_1", dv_switch_grid_model_id="dv_switch_1", sr_switch_grid_model_id={}, @@ -956,6 +993,7 @@ def test_asset_bay() -> None: # Test AssetBay with invalid sr_switch_grid_model_id type with pytest.raises(ValidationError): path = AssetBay( + asset_bay_id="station1::line5::bay", sl_switch_grid_model_id="sl_switch_1", dv_switch_grid_model_id="dv_switch_1", sr_switch_grid_model_id={"busbar1": 123}, # Invalid type @@ -964,6 +1002,7 @@ def test_asset_bay() -> None: def test_station_bay() -> None: path = AssetBay( + asset_bay_id="station1::line2::bay", sl_switch_grid_model_id="sl_switch_1", dv_switch_grid_model_id="dv_switch_1", sr_switch_grid_model_id={"busbar1": "sr_switch_1", "busbar2": "sr_switch_2"}, @@ -978,7 +1017,7 @@ def test_station_bay() -> None: ] assets = [ SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", asset_bay=path), + SwitchableAsset(grid_model_id="line2", asset_bay_id=path.asset_bay_id), SwitchableAsset(grid_model_id="line3"), ] asset_switching_table = np.array([[True, False, True], [False, True, False]]) @@ -989,6 +1028,7 @@ def test_station_bay() -> None: busbars=busbars, couplers=couplers, assets=assets, + asset_bays=[None, path, None], asset_switching_table=asset_switching_table, grid_model_id=grid_model_id, ) @@ -996,13 +1036,14 @@ def test_station_bay() -> None: # Test invalid AssetBay -> busbar3 is not in busbars path_error = AssetBay( + asset_bay_id="station1::line2::bay", sl_switch_grid_model_id="sl_switch_1", dv_switch_grid_model_id="dv_switch_1", sr_switch_grid_model_id={"busbar1": "sr_switch_1", "busbar3": "sr_switch_2"}, ) assets = [ SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", asset_bay=path_error), + SwitchableAsset(grid_model_id="line2", asset_bay_id=path_error.asset_bay_id), SwitchableAsset(grid_model_id="line3"), ] with pytest.raises(ValidationError, match="busbar_id busbar3 in asset line2 does not exist in busbars"): @@ -1010,6 +1051,7 @@ def test_station_bay() -> None: busbars=busbars, couplers=couplers, assets=assets, + asset_bays=[None, path_error, None], asset_switching_table=asset_switching_table, grid_model_id=grid_model_id, ) diff --git a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py index b111df9d0..37c8e8176 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py +++ b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py @@ -41,7 +41,7 @@ def build_raw_station( couplers: list[BusbarCoupler], asset_ids: list[str], asset_switching_table: np.ndarray, - asset_branch_ends: list[str | None] | None = None, + asset_terminals: list[str | None] | None = None, asset_bay_ids: list[str | None] | None = None, ) -> RawStation: """Build a raw station from explicit raw-topology fields. @@ -58,7 +58,7 @@ def build_raw_station( Grid model ids of the assets connected to the station. asset_switching_table : np.ndarray Busbar-to-asset switching matrix for the station. - asset_branch_ends : list[str | None] | None, optional + asset_terminals : list[str | None] | None, optional Optional branch-end metadata aligned with ``asset_ids``. asset_bay_ids : list[str | None] | None, optional Optional asset-bay metadata aligned with ``asset_ids``. @@ -77,7 +77,7 @@ def build_raw_station( busbars=busbars, couplers=couplers, asset_ids=asset_ids, - asset_branch_ends=asset_branch_ends if asset_branch_ends is not None else [None] * len(asset_ids), + asset_terminals=asset_terminals if asset_terminals is not None else [None] * len(asset_ids), asset_bay_ids=asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), asset_switching_table=asset_switching_table, asset_connectivity=None, diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index 27d1122e0..174c4fed6 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -40,7 +40,7 @@ def build_raw_station( couplers: list[BusbarCoupler], asset_ids: list[str], asset_switching_table: np.ndarray, - asset_branch_ends: list[str | None] | None = None, + asset_terminals: list[str | None] | None = None, asset_bay_ids: list[str | None] | None = None, ) -> RawStation: """Build a raw station from explicit raw-topology fields. @@ -57,7 +57,7 @@ def build_raw_station( Grid model ids of the assets connected to the station. asset_switching_table : np.ndarray Busbar-to-asset switching matrix for the station. - asset_branch_ends : list[str | None] | None, optional + asset_terminals : list[str | None] | None, optional Optional branch-end metadata aligned with ``asset_ids``. asset_bay_ids : list[str | None] | None, optional Optional asset-bay metadata aligned with ``asset_ids``. @@ -76,7 +76,7 @@ def build_raw_station( busbars=busbars, couplers=couplers, asset_ids=asset_ids, - asset_branch_ends=asset_branch_ends if asset_branch_ends is not None else [None] * len(asset_ids), + asset_terminals=asset_terminals if asset_terminals is not None else [None] * len(asset_ids), asset_bay_ids=asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), asset_switching_table=asset_switching_table, asset_connectivity=None, @@ -322,7 +322,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ) ] @@ -360,7 +360,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): busbars=busbars, couplers=[], asset_ids=["station_a_asset_0"], - asset_branch_ends=[None], + asset_terminals=[None], asset_bay_ids=[None], asset_switching_table=np.zeros((1, 1), dtype=bool), asset_connectivity=None, @@ -375,7 +375,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): busbars=busbars, couplers=[], asset_ids=["station_b_asset_0"], - asset_branch_ends=[None], + asset_terminals=[None], asset_bay_ids=[None], asset_switching_table=np.zeros((1, 1), dtype=bool), asset_connectivity=None, @@ -436,7 +436,7 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ) for asset_idx in range(n_assets) ] @@ -569,7 +569,7 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ), SwitchableAsset.model_construct( grid_model_id="station_save_load_asset_2", @@ -577,7 +577,7 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ), ] asset_switching_table = np.array([[True, False], [False, True]], dtype=bool) @@ -681,7 +681,7 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ), SwitchableAsset.model_construct( grid_model_id="asset2", @@ -689,7 +689,7 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ), ] asset_switching_table = np.array([[True, False], [False, True]], dtype=bool) diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index 2c931a2c9..15349d64c 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -95,7 +95,7 @@ def _build_large_random_action_set( name=None, in_service=True, branch_end=None, - asset_bay=None, + asset_bay_id=None, ) for asset_idx in range(n_assets) ] @@ -111,7 +111,7 @@ def _build_large_random_action_set( busbars=busbars, couplers=couplers, asset_ids=[asset.grid_model_id for asset in assets], - asset_branch_ends=[asset.branch_end for asset in assets], + asset_terminals=[None for _ in assets], asset_bay_ids=[asset.asset_bay_id for asset in assets], asset_switching_table=asset_switching_table, asset_connectivity=None, From 1b57b254978cb27504c29f5ed04dc72f566bf227 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 8 Jun 2026 14:11:58 +0200 Subject: [PATCH 08/24] fix: readd additional infos Signed-off-by: Leonard Hilfrich --- .../src/toop_engine_interfaces/asset_topology.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py index 08fb6f4f6..3d0d1e642 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py @@ -418,6 +418,12 @@ class _StationStructure(BaseModel): An entry is true if the asset is connected to the busbar. Note: An asset can be connected to multiple busbars, in which case a closed coupler is assumed to be present between these busbars. + Note: An asset can be connected to none of the busbars. In this case, the asset is intentionally + disconnected as part of a transmission line switching action. In practice, this usually involves + a separate switch from the asset-to-busbar couplers, as each asset usually has a switch that + completely disconnects it from the station. These switches are not modelled here, a + postprocessing routine needs to do the translation to this physical layout. Do not use + in_service for intentional disconnections. """ asset_connectivity: Optional[Bool[ArrayLike, "n_bus n_asset"]] = None @@ -428,7 +434,11 @@ class _StationStructure(BaseModel): """ model_log: Optional[list[str]] = None - """Holds log messages from the model creation process.""" + """Holds log messages from the model creation process. + + This can be used to store information about the model creation process, e.g. warnings or errors. + A potential use case is to inform the user about data quality issues e.g. missing the Asset Bay switches. + """ @field_validator("busbars") @classmethod From 526ebd9490454411f2650d01dbaec76572c6acf2 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 8 Jun 2026 14:47:56 +0200 Subject: [PATCH 09/24] fix: failing tests Signed-off-by: Leonard Hilfrich --- .../network_graph/graph_to_asset_topo.py | 3 ++- .../pandapower_import/pp_masks.py | 3 ++- .../network_graph/test_powsybl_station_to_graph.py | 7 ++----- .../src/toop_engine_interfaces/asset_topology.py | 12 ++++++++---- packages/interfaces_pkg/tests/test_asset_topology.py | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) 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 7f5dc9170..c54f6360e 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,8 @@ import pandera.typing as pat import structlog from beartype.typing import Literal, Optional, Union -from jaxtyping import ArrayLike, Bool +from jaxtyping import Bool +from numpy.typing import ArrayLike from toop_engine_importer.network_graph.data_classes import ( BranchSchema, BusbarConnectionInfo, diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/pp_masks.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/pp_masks.py index 54ab53fe5..599737fdc 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/pp_masks.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/pp_masks.py @@ -20,7 +20,8 @@ import pandapower as pp import structlog from beartype.typing import Optional -from jaxtyping import ArrayLike, Bool, Int +from jaxtyping import Bool, Int +from numpy.typing import ArrayLike from pandas import Index from toop_engine_importer.pandapower_import.pandapower_toolset_node_breaker import ( get_coupler_types_of_substation, 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 7be95e40c..4a6f532eb 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 @@ -161,12 +161,13 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assets = res.assets asset_bays = res.asset_bays + asset_terminals = res.asset_terminals assert len(assets) == 5 + assert asset_terminals == [None] * len(assets) assert assets[0].grid_model_id == "L3" assert assets[0].type == "LINE" assert assets[0].name == "" assert assets[0].in_service - assert assets[0].branch_end is None assert asset_bays[0].sl_switch_grid_model_id is None assert asset_bays[0].dv_switch_grid_model_id == "L32_BREAKER" assert asset_bays[0].sr_switch_grid_model_id == { @@ -178,7 +179,6 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[1].type == "LINE" assert assets[1].name == "" assert assets[1].in_service - assert assets[1].branch_end is None assert asset_bays[1].sl_switch_grid_model_id is None assert asset_bays[1].dv_switch_grid_model_id == "L62_BREAKER" assert asset_bays[1].sr_switch_grid_model_id == { @@ -190,7 +190,6 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[2].type == "LINE" assert assets[2].name == "" assert assets[2].in_service - assert assets[2].branch_end is None assert asset_bays[2].sl_switch_grid_model_id is None assert asset_bays[2].dv_switch_grid_model_id == "L72_BREAKER" assert asset_bays[2].sr_switch_grid_model_id == { @@ -202,7 +201,6 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[3].type == "LINE" assert assets[3].name == "" assert assets[3].in_service - assert assets[3].branch_end is None assert asset_bays[3].sl_switch_grid_model_id is None assert asset_bays[3].dv_switch_grid_model_id == "L91_BREAKER" assert asset_bays[3].sr_switch_grid_model_id == { @@ -214,7 +212,6 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert assets[4].type == "LOAD" assert assets[4].name == "" assert assets[4].in_service - assert assets[4].branch_end is None assert asset_bays[4].sl_switch_grid_model_id is None assert asset_bays[4].dv_switch_grid_model_id == "load2_BREAKER" assert asset_bays[4].sr_switch_grid_model_id == { diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py index 3d0d1e642..32996829a 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py @@ -12,8 +12,10 @@ import numpy as np from beartype.typing import Any, Literal, Optional, TypeAlias, Union, get_args -from jaxtyping import ArrayLike, Bool -from pydantic import BaseModel, Field, field_validator, model_validator +from numpydantic import NDArray, Shape +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +StationSwitchingArray: TypeAlias = NDArray[Shape["* n_bus, * n_asset"], np.bool_] class PowsyblSwitchValues(Enum): @@ -383,6 +385,8 @@ def _validate_station_physical_assignments( class _StationStructure(BaseModel): """Shared station fields and structural validators for station views.""" + model_config = ConfigDict(arbitrary_types_allowed=True) + grid_model_id: str """The unique identifier of the station. @@ -412,7 +416,7 @@ class _StationStructure(BaseModel): couplers: list[BusbarCoupler] """The list of couplers at the station.""" - asset_switching_table: Bool[ArrayLike, "n_bus n_asset"] + asset_switching_table: StationSwitchingArray """Holds the switching of each asset to each busbar, shape (n_bus, n_asset). An entry is true if the asset is connected to the busbar. @@ -426,7 +430,7 @@ class _StationStructure(BaseModel): in_service for intentional disconnections. """ - asset_connectivity: Optional[Bool[ArrayLike, "n_bus n_asset"]] = None + asset_connectivity: Optional[StationSwitchingArray] = None """Holds the all possible layouts of the asset_switching_table, shape (n_bus, n_asset). An entry is true if it is possible to connect an asset to the busbar. diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index 4e1bc109a..b1c4ac56d 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -459,11 +459,11 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: assets=[ SwitchableAsset( grid_model_id="line1", - branch_end="from", asset_bay_id="station1::line1::bay", ), SwitchableAsset(grid_model_id="load1"), ], + asset_terminals=["from", None], asset_bays=[ AssetBay( asset_bay_id="station1::line1::bay", From 1836799c42c6742fb08ce16f1442fbb74c1039a9 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 8 Jun 2026 14:48:52 +0200 Subject: [PATCH 10/24] fix: even more tests Signed-off-by: Leonard Hilfrich --- packages/interfaces_pkg/tests/test_stored_action_set.py | 6 ------ .../tests/test_stored_action_set_performance.py | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index 174c4fed6..05d1c0273 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -321,7 +321,6 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ) ] @@ -435,7 +434,6 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ) for asset_idx in range(n_assets) @@ -568,7 +566,6 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ), SwitchableAsset.model_construct( @@ -576,7 +573,6 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ), ] @@ -680,7 +676,6 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ), SwitchableAsset.model_construct( @@ -688,7 +683,6 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ), ] diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index 15349d64c..05f970394 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -94,7 +94,6 @@ def _build_large_random_action_set( type=None, name=None, in_service=True, - branch_end=None, asset_bay_id=None, ) for asset_idx in range(n_assets) From 387009cc49dd96ae343a6210a495159ade95bcc5 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 8 Jun 2026 15:09:31 +0200 Subject: [PATCH 11/24] fix: notebook tests Signed-off-by: Leonard Hilfrich --- notebooks/example3_e2e_pipeline.ipynb | 6 +++--- .../tests/export/test_asset_topology_to_dgs.py | 6 +----- .../benchmark/benchmark_utils.py | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/notebooks/example3_e2e_pipeline.ipynb b/notebooks/example3_e2e_pipeline.ipynb index 866fdabf2..efa3e5112 100644 --- a/notebooks/example3_e2e_pipeline.ipynb +++ b/notebooks/example3_e2e_pipeline.ipynb @@ -360,9 +360,9 @@ " \"k_best_topos\": 3 # Change this value to use a different number of topologies\n", "})\n", "\n", - "# Hint: optimisation_run_dir should contain the path to the folder which contains the valid res.json file.\n", - "optimisation_run_dir = f\"../data/{optimizer_snapshot_dir}/run_0\"\n", - "optimisation_run_dir = Path(optimisation_run_dir)\n", + "# Reuse the run directory generated in the earlier optimization cell so the action set\n", + "# and the optimizer results stay aligned during this notebook execution.\n", + "optimisation_run_dir = Path(run_dir)\n", "\n", "topo_paths = run_pipeline(\n", " pipeline_cfg=pipeline_cfg,\n", diff --git a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py index 7621f6408..6fcb0e101 100644 --- a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py +++ b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py @@ -199,11 +199,7 @@ def test_get_asset_bay_sr_fid_list(basic_node_breaker_topology): ] assert asset_bay_sr_fid_list == expected - station.assets[1] = station.assets[1].model_copy( - update={ - "asset_bay": None, - } - ) + station.asset_bays[1] = None asset_bay_sr_fid_list = get_asset_bay_grid_model_id_list(station) expected = [ {"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py index a7d599a59..8b975a577 100644 --- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py +++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py @@ -27,7 +27,7 @@ # Domain-specific imports (may raise if not available in the environment) import pypowsybl import structlog -from beartype.typing import Literal, Optional, Tuple +from beartype.typing import Any, Literal, Optional, Tuple from fsspec.implementations.dirfs import DirFileSystem from fsspec.implementations.local import LocalFileSystem from omegaconf import DictConfig @@ -97,7 +97,7 @@ def suppress_jax_logs() -> None: """Disables jax debug logs spamming the console""" - def _drop_jax_logs(_logger, _method_name, event_dict: dict[str, str]) -> dict[str, str]: # noqa: ANN001 + def _drop_jax_logs(_logger, _method_name, event_dict: dict[str, Any]) -> dict[str, Any]: # noqa: ANN001 logger_name = event_dict.get("logger", "") if logger_name.startswith(("jax", "jaxlib", "xla", "absl")): raise DropEvent From 5f2e5a263818f2ff40fbeb3e196ba666e46e8d94 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 8 Jun 2026 15:20:43 +0200 Subject: [PATCH 12/24] fix: notebook paths Signed-off-by: Leonard Hilfrich --- notebooks/example2_small_grid_toop.ipynb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/notebooks/example2_small_grid_toop.ipynb b/notebooks/example2_small_grid_toop.ipynb index 55191fc3d..f6eec07f8 100644 --- a/notebooks/example2_small_grid_toop.ipynb +++ b/notebooks/example2_small_grid_toop.ipynb @@ -279,12 +279,13 @@ "import pypowsybl as pp\n", "from IPython.display import SVG, display\n", "\n", - "# Adjust run directory\n", - "run_dir = 'run_0'\n", + "# Reuse the topology directory generated by the earlier pipeline cell.\n", + "run_path = topo_path[0]\n", "# Find name of the voltage level (substation) that was adjusted\n", "# The SLD of the adjusted substation is named as \"_sld.svg\"\n", "voltage_level_name = 'VLevel2'\n", - "sld_path = optimizer_snapshot_dir / run_dir / 'topology_0' / \"sld\" / f\"{voltage_level_name}_sld_initial.svg\"\n", + "sld_dir = run_path / 'sld'\n", + "sld_path = sld_dir / f\"{voltage_level_name}_sld_initial.svg\"\n", "\n", "# Load initial grid into pypowsybl\n", "network = pp.network.load(str(iteration_name / file_path))\n", @@ -300,7 +301,7 @@ "# Display SLD before optimization\n", "display(SVG(filename=sld_path))\n", "# Display SLD after optimization\n", - "display(SVG(filename=optimizer_snapshot_dir / run_dir / 'topology_0' / \"sld\" / f\"{voltage_level_name}_sld.svg\"))\n" + "display(SVG(filename=sld_dir / f\"{voltage_level_name}_sld.svg\"))\n" ] }, { From 827ed8b5d643e4c87e8b63b1d2026d0d51a8af28 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Thu, 11 Jun 2026 07:32:29 +0200 Subject: [PATCH 13/24] asset_id to asset_bay on station Signed-off-by: Leonard Hilfrich --- .../toop_engine_dc_solver/example_classes.py | 9 +- .../toop_engine_dc_solver/example_grids.py | 3 +- .../export/disconnection_switch_updates.py | 4 +- .../export/station_switch_updates.py | 9 +- .../apply_asset_topo_pandapower.py | 3 +- .../postprocess/apply_asset_topo_powsybl.py | 20 +- .../postprocess/postprocess_pandapower.py | 5 +- .../postprocess/postprocess_powsybl.py | 5 +- .../preprocess/preprocess_bb_outage.py | 4 +- .../preprocess/preprocess_switching.py | 10 +- packages/dc_solver_pkg/tests/conftest.py | 8 +- .../export/test_asset_topology_to_dgs.py | 10 +- .../export/test_station_switch_updates.py | 4 +- .../initial_topology/asset_topology.json | 100 ++-- .../test_apply_asset_topo_powsybl.py | 24 +- .../test_preprocess_bb_outage.py | 10 +- .../test_preprocess_switching.py | 80 ++- .../powsybl/powsybl_asset_topo.py | 41 +- .../tests/powsybl/test_powsybl_asset_topo.py | 23 +- .../exporter/asset_topology_to_ucte.py | 2 +- .../network_graph/powsybl_station_to_graph.py | 22 +- .../pandapower_import/asset_topology.py | 10 +- .../pandapower_import/preprocessing.py | 11 +- .../test_powsybl_station_to_graph.py | 28 +- .../pypowsybl_import/test_preprocessing.py | 9 +- .../toop_engine_interfaces/asset_topology.py | 207 ++++---- .../asset_topology_helpers.py | 117 +++-- .../tests/test_asset_topology.py | 455 +++++++++--------- .../tests/test_asset_topology_helpers.py | 107 ++-- .../tests/test_stored_action_set.py | 58 ++- .../test_stored_action_set_performance.py | 13 +- 31 files changed, 782 insertions(+), 629 deletions(-) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py index c70cfb904..47d4f6c4f 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py @@ -15,6 +15,7 @@ Busbar, BusbarCoupler, RawStation, + StationAssetConnection, SwitchableAsset, Topology, ) @@ -55,9 +56,11 @@ def get_basic_node_breaker_topology() -> Topology: in_service=True, ) ], - asset_ids=["L4", "L5", "L8"], - asset_terminals=[None, None, None], - asset_bay_ids=["VL4_0::L4::bay", "VL4_0::L5::bay", "VL4_0::L8::bay"], + asset_connections=[ + StationAssetConnection(asset_id="L4", terminal=None, asset_bay_id="VL4_0::L4::bay"), + StationAssetConnection(asset_id="L5", terminal=None, asset_bay_id="VL4_0::L5::bay"), + StationAssetConnection(asset_id="L8", terminal=None, asset_bay_id="VL4_0::L8::bay"), + ], asset_switching_table=np.array([[False, False, False], [True, True, False]]), asset_connectivity=np.array([[True, True, True], [True, True, True]]), ) 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 4e75d3da6..f1cabce69 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 @@ -54,6 +54,7 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, SwitchableAsset, Topology, @@ -279,7 +280,7 @@ def random_station_info_backend( open=False, ), ], - assets=switchable_assets, + asset_connections=[MaterializedAssetConnection(asset=asset) for asset in switchable_assets], asset_switching_table=asset_switching_table, asset_connectivity=np.ones_like(asset_switching_table, dtype=bool), ), pp_counters diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py index 4d6fbb805..d349182f9 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py @@ -47,7 +47,9 @@ def get_disconnected_asset_ids( disconnection_map: dict[str, GridElement] = {disconnection.id: disconnection for disconnection in disconnections} disconnection_asset_map: dict[str, list[AssetBay]] = {disconnection.id: [] for disconnection in disconnections} for station in stations: - for asset, asset_bay in zip(station.assets, station.asset_bays, strict=True): + for asset_connection in station.asset_connections: + asset = asset_connection.asset + asset_bay = asset_connection.asset_bay if asset.grid_model_id in disconnection_map and asset_bay is not None: corresponding_disconnection = disconnection_map[asset.grid_model_id] disconnection_asset_map[corresponding_disconnection.id].append(asset_bay) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py index 9436b845d..ff7740c1f 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py @@ -159,8 +159,8 @@ def _get_asset_switch_diffs( ) changed_busbar_lookup = _get_busbar_lookup(changed_station) - changed_asset_ids = [asset.grid_model_id for asset in changed_station.assets] - starting_asset_ids = [asset.grid_model_id for asset in starting_station.assets] + changed_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in changed_station.asset_connections] + starting_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in starting_station.asset_connections] if changed_asset_ids != starting_asset_ids: raise ValueError( "Changed station assets are not ordered like the starting topology for station " @@ -170,8 +170,9 @@ def _get_asset_switch_diffs( switching_xor = np.logical_xor(starting_switching_table, changed_switching_table) diff_switches: list[dict[str, str | bool]] = [] - for column, changed_asset in enumerate(changed_station.assets): - asset_bay = changed_station.asset_bays[column] + for column, changed_asset_connection in enumerate(changed_station.asset_connections): + changed_asset = changed_asset_connection.asset + asset_bay = changed_asset_connection.asset_bay if asset_bay is None: continue changed_switch_states = changed_switching_table[:, column] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py index 889ab323c..0803036d1 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py @@ -313,7 +313,8 @@ def apply_station_assets( disconnection_diff = [] reassignment_diff = [] - for asset_index, asset in enumerate(station.assets): + for asset_index, asset_connection in enumerate(station.asset_connections): + asset = asset_connection.asset pp_id, asset_type = parse_globally_unique_id(asset.grid_model_id) pp_table = get_element_table(asset_type) pp_id = int(pp_id) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index 1f6106478..83a91c3e8 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -60,7 +60,8 @@ def get_coupler_states_from_busbar_couplers(station_couplers: list[BusbarCoupler def get_asset_bay_grid_model_id_list(station: MaterializedStation) -> list[dict[str, str] | None]: """Get selector switch ids for each station asset.""" sr_switch_grid_model_id_list: list[dict[str, str] | None] = [] - for asset_bay in station.asset_bays: + for asset_connection in station.asset_connections: + asset_bay = asset_connection.asset_bay if asset_bay is None or asset_bay.sr_switch_grid_model_id is None: sr_switch_grid_model_id_list.append(None) else: @@ -104,7 +105,7 @@ def get_asset_switch_states_from_station( } ) elif active_busbars == 0: - asset_bay = station.asset_bays[column] + asset_bay = station.asset_connections[column].asset_bay assert asset_bay is not None switch_disconnection_list.append( { @@ -353,7 +354,7 @@ def apply_single_asset_bus_branch( station : Station The asset topology station in which the asset at position asset_index will be applied to the powsybl network. asset_index : int - The index of the asset in the station.assets list. + The index of the asset connection in the station switching table. Returns ------- @@ -374,8 +375,9 @@ def apply_single_asset_bus_branch( """ vl_id = net.get_buses(attributes=["voltage_level_id"]).loc[station.grid_model_id]["voltage_level_id"] + asset = station.asset_connections[asset_index].asset is_branch, is_connected, from_side, bus_breaker_id = find_asset( - net=net, elem_id=station.assets[asset_index].grid_model_id, voltage_level_id=vl_id, bus_id=station.grid_model_id + net=net, elem_id=asset.grid_model_id, voltage_level_id=vl_id, bus_id=station.grid_model_id ) switching_column = station.asset_switching_table[:, asset_index] @@ -396,22 +398,22 @@ def apply_single_asset_bus_branch( if target_disconnected and is_connected: if is_branch: - disconnect_branch(net=net, elem_id=station.assets[asset_index].grid_model_id) + disconnect_branch(net=net, elem_id=asset.grid_model_id) else: - disconnect_injection(net=net, elem_id=station.assets[asset_index].grid_model_id) + disconnect_injection(net=net, elem_id=asset.grid_model_id) return "disconnected", [] if station.busbars[target_bus_index].grid_model_id != bus_breaker_id: if is_branch: move_branch( net=net, - elem_id=station.assets[asset_index].grid_model_id, + elem_id=asset.grid_model_id, bus_breaker_id=station.busbars[target_bus_index].grid_model_id, from_end=from_side, ) else: move_injection( net=net, - elem_id=station.assets[asset_index].grid_model_id, + elem_id=asset.grid_model_id, bus_breaker_id=station.busbars[target_bus_index].grid_model_id, ) reassignments = [(asset_index, target_bus_index, True)] @@ -489,7 +491,7 @@ def apply_station_bus_branch(net: Network, station: MaterializedStation) -> Appl reassignment_diff = [] coupler_diff = [] - for asset_index in range(len(station.assets)): + for asset_index in range(len(station.asset_connections)): result, new_reassignments = apply_single_asset_bus_branch(net, station, asset_index) if result == "disconnected": disconnection_diff.append(asset_index) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py index d017dd847..30db75ac0 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py @@ -193,14 +193,15 @@ def compute_cross_coupler_flows( assert len(components) == 2, "The split station must have exactly two sides" busbars_a = list(components[0]) - for index, asset in enumerate(station.assets): + for index, asset_connection in enumerate(station.asset_connections): + asset = asset_connection.asset asset_id, asset_type = parse_globally_unique_id(asset.grid_model_id) asset_id = int(asset_id) if station.asset_switching_table[busbars_a, index].any(): # The asset is on busbar A, include it if asset.is_branch() is True: - branch_end = station.asset_terminals[index] + branch_end = asset_connection.terminal if branch_end is None: raise ValueError("Branch end must be set in asset topo") from_end = branch_end in ("from", "hv") diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index 507c5a0ea..10ef860c2 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -266,11 +266,12 @@ def compute_cross_coupler_flows( p_sum = 0.0 q_sum = 0.0 - for index, asset in enumerate(station.assets): + for index, asset_connection in enumerate(station.asset_connections): + asset = asset_connection.asset if station.asset_switching_table[busbars_a, index].any(): # The asset is on busbar A, include it if asset.is_branch() is True: - branch_end = station.asset_terminals[index] + branch_end = asset_connection.terminal if branch_end is None: raise ValueError("Branch end is None") from_end = branch_end in ("from", "hv") diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py index 02dafbb34..3648838f4 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py @@ -206,8 +206,8 @@ def get_busbar_map_adjacent_branches(network_data: NetworkData) -> Bool[np.ndarr for station_id in network_data.busbar_outage_map.keys(): # Find the asset topo station id station = find_station_by_id(network_data.asset_topology.materialize_stations(), station_id) - for asset in station.assets: - bb_outage_asset_indices.add(asset.grid_model_id) + for asset_connection in station.asset_connections: + bb_outage_asset_indices.add(asset_connection.asset.grid_model_id) busbar_outage_branch_mask = np.array([(id in bb_outage_asset_indices) for id in network_data.branch_ids]) return busbar_outage_branch_mask diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py index 620208365..13f4459cd 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py @@ -124,9 +124,9 @@ def make_separation_set( """ assert all(busbar.in_service for busbar in station.busbars) assert all(coupler.in_service for coupler in station.couplers) - assert all(asset.in_service for asset in station.assets) + assert all(asset_connection.asset.in_service for asset_connection in station.asset_connections) if not len(station.couplers): - return (np.zeros((0, 2, len(station.assets)), dtype=bool), np.zeros((0, 0), dtype=bool), []) + return (np.zeros((0, 2, len(station.asset_connections)), dtype=bool), np.zeros((0, 0), dtype=bool), []) # Go through all possible coupler assignments and find all the configurations in which there are # exactly two electrical busbars. switching_map = { @@ -440,13 +440,15 @@ def add_missing_asset_topology_branch_info( new_raw_stations = [] for station in asset_topology.raw_stations: new_branch_ends = [] - for asset_id, branch_end in zip(station.asset_ids, station.asset_terminals, strict=True): + for asset_connection in station.asset_connections: + asset_id = asset_connection.asset_id + branch_end = asset_connection.terminal index = branch_id_lookup.get(asset_id, None) if index is not None and (overwrite_if_present or branch_end is None): new_branch_ends.append("from" if branch_from_nodes[index] == station.grid_model_id else "to") else: new_branch_ends.append(branch_end) - new_raw_stations.append(station.model_copy(update={"asset_terminals": new_branch_ends})) + new_raw_stations.append(station.with_asset_terminals(new_branch_ends)) return copy_topology_with_updates( reference_topology=asset_topology, diff --git a/packages/dc_solver_pkg/tests/conftest.py b/packages/dc_solver_pkg/tests/conftest.py index df6c454f4..04561fb03 100644 --- a/packages/dc_solver_pkg/tests/conftest.py +++ b/packages/dc_solver_pkg/tests/conftest.py @@ -97,6 +97,7 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, SwitchableAsset, Topology, @@ -909,7 +910,12 @@ def mock_station() -> MaterializedStation: in_service=True, ), ], - assets=[asset1, asset2, asset3, asset4], + asset_connections=[ + MaterializedAssetConnection(asset=asset1), + MaterializedAssetConnection(asset=asset2), + MaterializedAssetConnection(asset=asset3), + MaterializedAssetConnection(asset=asset4), + ], asset_switching_table=np.array( [ [True, False, True, False], # Busbar 0 diff --git a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py index 6fcb0e101..b6ba11713 100644 --- a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py +++ b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py @@ -199,7 +199,15 @@ def test_get_asset_bay_sr_fid_list(basic_node_breaker_topology): ] assert asset_bay_sr_fid_list == expected - station.asset_bays[1] = None + station = station.model_copy( + update={ + "asset_connections": [ + station.asset_connections[0], + station.asset_connections[1].model_copy(update={"asset_bay": None}), + station.asset_connections[2], + ] + } + ) asset_bay_sr_fid_list = get_asset_bay_grid_model_id_list(station) expected = [ {"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, diff --git a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py index 9c4886f1e..d4bcbd005 100644 --- a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py @@ -110,8 +110,8 @@ def test_get_asset_switch_diffs_requires_matching_switching_table_shape(basic_no def test_get_asset_switch_diffs_requires_matching_asset_order(basic_node_breaker_topology): station = basic_node_breaker_topology.materialize_stations()[0] - reordered_assets = [station.assets[1], station.assets[0], station.assets[2]] - changed_station = station.model_copy(update={"assets": reordered_assets}) + reordered_asset_connections = [station.asset_connections[1], station.asset_connections[0], station.asset_connections[2]] + changed_station = station.model_copy(update={"asset_connections": reordered_asset_connections}) with pytest.raises(ValueError, match="Use ActionSet.simplified_starting_topology as input"): _get_asset_switch_diffs( 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 eb10486e8..e7a4a8fd4 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 @@ -110,29 +110,37 @@ ] ], "model_log": [], - "asset_ids": [ - "L1", - "L2", - "L6", - "L7", - "L8", - "load1" - ], - "asset_terminals": [ - null, - null, - null, - null, - null, - null - ], - "asset_bay_ids": [ - "VL2_0::L1::bay", - "VL2_0::L2::bay", - "VL2_0::L6::bay", - "VL2_0::L7::bay", - "VL2_0::L8::bay", - "VL2_0::load1::bay" + "asset_connections": [ + { + "asset_id": "L1", + "terminal": null, + "asset_bay_id": "VL2_0::L1::bay" + }, + { + "asset_id": "L2", + "terminal": null, + "asset_bay_id": "VL2_0::L2::bay" + }, + { + "asset_id": "L6", + "terminal": null, + "asset_bay_id": "VL2_0::L6::bay" + }, + { + "asset_id": "L7", + "terminal": null, + "asset_bay_id": "VL2_0::L7::bay" + }, + { + "asset_id": "L8", + "terminal": null, + "asset_bay_id": "VL2_0::L8::bay" + }, + { + "asset_id": "load1", + "terminal": null, + "asset_bay_id": "VL2_0::load1::bay" + } ] }, { @@ -268,26 +276,32 @@ ] ], "model_log": [], - "asset_ids": [ - "L3", - "L6", - "L7", - "L9", - "load2" - ], - "asset_terminals": [ - null, - null, - null, - null, - null - ], - "asset_bay_ids": [ - "VL3_0::L3::bay", - "VL3_0::L6::bay", - "VL3_0::L7::bay", - "VL3_0::L9::bay", - "VL3_0::load2::bay" + "asset_connections": [ + { + "asset_id": "L3", + "terminal": null, + "asset_bay_id": "VL3_0::L3::bay" + }, + { + "asset_id": "L6", + "terminal": null, + "asset_bay_id": "VL3_0::L6::bay" + }, + { + "asset_id": "L7", + "terminal": null, + "asset_bay_id": "VL3_0::L7::bay" + }, + { + "asset_id": "L9", + "terminal": null, + "asset_bay_id": "VL3_0::L9::bay" + }, + { + "asset_id": "load2", + "terminal": null, + "asset_bay_id": "VL3_0::load2::bay" + } ] } ], diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py index c98c7c98a..ced6a2cee 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py @@ -59,7 +59,7 @@ def test_find_asset(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: bus_id = station.grid_model_id vl_id = net.get_buses().loc[bus_id]["voltage_level_id"] - for elem in station.assets: + for elem in [asset_connection.asset for asset_connection in station.asset_connections]: is_branch, connected, direction, bus_breaker_id = find_asset( net=net, elem_id=elem.grid_model_id, @@ -82,7 +82,7 @@ def test_find_asset(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: assert inj["bus_id"] == bus_id assert inj["bus_breaker_bus_id"] == bus_breaker_id - for elem in topology.materialize_stations()[10].assets: + for elem in [asset_connection.asset for asset_connection in topology.materialize_stations()[10].asset_connections]: if elem.is_branch(): with pytest.raises(ValueError, match=f"Branch {elem.grid_model_id} not found in the station"): find_asset( @@ -106,7 +106,7 @@ def test_apply_single_asset_bus_branch_reassign(case14_data_with_asset_topo: tup net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) base_station = topology.materialize_stations()[0] - for asset_index in range(len(base_station.assets)): + for asset_index in range(len(base_station.asset_connections)): station = base_station.model_copy(deep=True) # Reassign the first asset to the second bus assert len(station.busbars) == 2 @@ -129,11 +129,12 @@ def test_apply_single_asset_bus_branch_reassign(case14_data_with_asset_topo: tup ] assert len(new_bus) == 1 new_bus = new_bus[0] - if station.assets[asset_index].is_branch(): - brh = net.get_branches(all_attributes=True).loc[station.assets[asset_index].grid_model_id] + asset = station.asset_connections[asset_index].asset + if asset.is_branch(): + brh = net.get_branches(all_attributes=True).loc[asset.grid_model_id] assert brh["bus_breaker_bus1_id"] == new_bus.grid_model_id or brh["bus_breaker_bus2_id"] == new_bus.grid_model_id else: - inj = net.get_injections(all_attributes=True).loc[station.assets[asset_index].grid_model_id] + inj = net.get_injections(all_attributes=True).loc[asset.grid_model_id] assert inj["bus_breaker_bus_id"] == new_bus.grid_model_id @@ -142,7 +143,7 @@ def test_apply_single_asset_bus_branch_disconnect(case14_data_with_asset_topo: t net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) base_station = topology.materialize_stations()[0] - for asset_index in range(len(base_station.assets)): + for asset_index in range(len(base_station.asset_connections)): station = base_station.model_copy(deep=True) # Disconnect the first asset assert len(station.busbars) == 2 @@ -155,11 +156,12 @@ def test_apply_single_asset_bus_branch_disconnect(case14_data_with_asset_topo: t assert status == "disconnected" assert len(reassignments) == 0 - if station.assets[asset_index].is_branch(): - brh = net.get_branches(all_attributes=True).loc[station.assets[asset_index].grid_model_id] + asset = station.asset_connections[asset_index].asset + if asset.is_branch(): + brh = net.get_branches(all_attributes=True).loc[asset.grid_model_id] assert not brh["connected1"] and not brh["connected2"] else: - inj = net.get_injections(all_attributes=True).loc[station.assets[asset_index].grid_model_id] + inj = net.get_injections(all_attributes=True).loc[asset.grid_model_id] assert not inj["connected"] @@ -168,7 +170,7 @@ def test_apply_single_asset_bus_branch_nothing(case14_data_with_asset_topo: tupl net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) base_station = topology.materialize_stations()[0] - for asset_index in range(len(base_station.assets)): + for asset_index in range(len(base_station.asset_connections)): status, reassignments = apply_single_asset_bus_branch( net=net, station=base_station, diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py index 176474300..95008e274 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py @@ -26,7 +26,7 @@ update_network_data_with_non_rel_bb_outages, ) from toop_engine_dc_solver.preprocess.preprocess_station_realisations import enumerate_station_realisations -from toop_engine_interfaces.asset_topology import Busbar, MaterializedStation, SwitchableAsset +from toop_engine_interfaces.asset_topology import Busbar, MaterializedAssetConnection, MaterializedStation, SwitchableAsset def test_get_total_injection_along_stub_branch(network_data: NetworkData): @@ -235,7 +235,7 @@ def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): grid_model_id="node_2", busbars=[busbar_0, busbar_1], couplers=[], - assets=[asset2, asset3, asset4, asset7], + asset_connections=[MaterializedAssetConnection(asset=asset) for asset in [asset2, asset3, asset4, asset7]], asset_switching_table=np.array( [ [True, False, True, False], # Busbar 0 @@ -326,7 +326,7 @@ def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): grid_model_id="node_0", busbars=[busbar_0], couplers=[], - assets=[asset1, asset4, asset5, asset6], + asset_connections=[MaterializedAssetConnection(asset=asset) for asset in [asset1, asset4, asset5, asset6]], asset_switching_table=np.array( [ [True, True, True, True], # Busbar 0 @@ -393,8 +393,8 @@ def test_update_network_data_with_non_rel_bb_outages(network_data_preprocessed: for branch_index in branch_outages: branch_id = updated_net_data.branch_ids[branch_index] # get asset_index of the branch - for asset_index, asset in enumerate(station.assets): - if asset.grid_model_id == branch_id: + for asset_index, asset_connection in enumerate(station.asset_connections): + if asset_connection.asset.grid_model_id == branch_id: break assert station.asset_switching_table[busbar_index, asset_index], ( diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py index e48f0eb56..97c744a23 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py @@ -5,6 +5,7 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 +import json from pathlib import Path import jax.numpy as jnp @@ -26,13 +27,30 @@ AssetInjectionType, Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, SwitchableAsset, ) +def build_materialized_station( + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + assets: list[SwitchableAsset], + asset_switching_table: np.ndarray, +) -> MaterializedStation: + return MaterializedStation( + grid_model_id=grid_model_id, + busbars=busbars, + couplers=couplers, + asset_connections=[MaterializedAssetConnection(asset=asset) for asset in assets], + asset_switching_table=asset_switching_table, + ) + + def test_make_configurations_table(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -69,7 +87,7 @@ def test_make_configurations_table(): assert problems.disconnected_busbars is None assert problems.duplicate_couplers is None assert preprocessed_station.busbars == station.busbars - assert preprocessed_station.assets == station.assets + assert preprocessed_station.asset_connections == station.asset_connections assert len(preprocessed_station.couplers) == 3 assert all([not coupler.open for coupler in preprocessed_station.couplers]) @@ -138,7 +156,7 @@ def test_identify_unnecessary_combinations() -> None: def test_preprocess_station() -> None: - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -182,7 +200,7 @@ def test_preprocess_station() -> None: def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> None: - num_assets_before = sum(len(station.assets) for station in network_data.asset_topology.materialize_stations()) + num_assets_before = sum(len(station.asset_connections) for station in network_data.asset_topology.materialize_stations()) topo = add_missing_asset_topology_branch_info( asset_topology=network_data.asset_topology, @@ -196,12 +214,13 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No from_ends = 0 to_ends = 0 for station in topo.materialize_stations(): - for index, asset in enumerate(station.assets): + for asset_connection in station.asset_connections: + asset = asset_connection.asset if asset.grid_model_id in network_data.branch_ids: assert asset.name in network_data.branch_names assert asset.type in network_data.branch_types - assert station.asset_terminals[index] in ["from", "to"] - if station.asset_terminals[index] == "from": + assert asset_connection.terminal in ["from", "to"] + if asset_connection.terminal == "from": from_ends += 1 else: to_ends += 1 @@ -209,12 +228,12 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No assert from_ends > 0 assert to_ends > 0 - num_assets_after = sum(len(station.assets) for station in topo.materialize_stations()) + num_assets_after = sum(len(station.asset_connections) for station in topo.materialize_stations()) assert num_assets_before == num_assets_after def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> None: - num_assets_before = sum(len(station.assets) for station in network_data.asset_topology.materialize_stations()) + num_assets_before = sum(len(station.asset_connections) for station in network_data.asset_topology.materialize_stations()) topo = add_missing_asset_topology_injection_info( asset_topology=network_data.asset_topology, @@ -225,12 +244,12 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> ) for station in topo.materialize_stations(): - for asset in station.assets: + for asset in [asset_connection.asset for asset_connection in station.asset_connections]: if asset.grid_model_id in network_data.injection_ids: assert asset.name in network_data.injection_names assert asset.type in network_data.injection_types - num_assets_after = sum(len(station.assets) for station in topo.materialize_stations()) + num_assets_after = sum(len(station.asset_connections) for station in topo.materialize_stations()) assert num_assets_before == num_assets_after # Test with overwrite_if_present=False @@ -243,7 +262,7 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> ) for station in topo.materialize_stations(): - for asset in station.assets: + for asset in [asset_connection.asset for asset_connection in station.asset_connections]: if asset.grid_model_id in network_data.injection_ids: assert asset.name in network_data.injection_names assert asset.type in network_data.injection_types @@ -257,7 +276,7 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> ) for station in topo.materialize_stations(): - for asset in station.assets: + for asset in [asset_connection.asset for asset_connection in station.asset_connections]: if asset.grid_model_id in network_data.injection_ids: assert asset.name == "new_name" assert asset.type == "new_type" @@ -266,7 +285,16 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> def test_prepare_for_separation_set_node_breaker_test_station(): file = Path(__file__).parent.parent / "files" / "test_station.json" with open(file, "r") as f: - station = MaterializedStation.model_validate_json(f.read()) + station_data = json.load(f) + + assets = station_data.pop("assets") + asset_terminals = station_data.pop("asset_terminals", [None] * len(assets)) + asset_bays = station_data.pop("asset_bays", [None] * len(assets)) + station_data["asset_connections"] = [ + {"asset": asset, "terminal": asset_terminal, "asset_bay": asset_bay} + for asset, asset_terminal, asset_bay in zip(assets, asset_terminals, asset_bays, strict=True) + ] + station = MaterializedStation.model_validate(station_data) x = nx.Graph() for busbar in station.busbars: @@ -281,8 +309,16 @@ def test_prepare_for_separation_set_node_breaker_test_station(): with pytest.raises(ValueError, match="no couplers left after preprocessing"): prepare_for_separation_set( station, - branch_ids=[asset.grid_model_id for asset in station.assets if asset.type in get_args(AssetBranchType)], - injection_ids=[asset.grid_model_id for asset in station.assets if asset.type in get_args(AssetInjectionType)], + branch_ids=[ + asset_connection.asset.grid_model_id + for asset_connection in station.asset_connections + if asset_connection.asset.type in get_args(AssetBranchType) + ], + injection_ids=[ + asset_connection.asset.grid_model_id + for asset_connection in station.asset_connections + if asset_connection.asset.type in get_args(AssetInjectionType) + ], ) station = station.model_copy( @@ -293,8 +329,16 @@ def test_prepare_for_separation_set_node_breaker_test_station(): preprocessed_station, problems = prepare_for_separation_set( station, - branch_ids=[asset.grid_model_id for asset in station.assets if asset.type in get_args(AssetBranchType)], - injection_ids=[asset.grid_model_id for asset in station.assets if asset.type in get_args(AssetInjectionType)], + branch_ids=[ + asset_connection.asset.grid_model_id + for asset_connection in station.asset_connections + if asset_connection.asset.type in get_args(AssetBranchType) + ], + injection_ids=[ + asset_connection.asset.grid_model_id + for asset_connection in station.asset_connections + if asset_connection.asset.type in get_args(AssetInjectionType) + ], close_couplers=True, ) assert len(preprocessed_station.couplers) diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index e1003a6c4..2a488496b 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -21,11 +21,11 @@ from pypowsybl.network.impl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_helpers import change_dangling_to_tie, get_voltage_level_with_region from toop_engine_interfaces.asset_topology import ( - AssetBay, Busbar, BusbarCoupler, MaterializedStation, RawStation, + StationAssetConnection, SwitchableAsset, Topology, ) @@ -127,8 +127,6 @@ def get_list_of_coupler_from_df(coupler_elements: pd.DataFrame) -> list[BusbarCo def get_list_of_switchable_assets_from_df( station_branches: pd.DataFrame, - asset_bay_list: Optional[list[AssetBay]] = None, - asset_bay_dict: Optional[dict[str, AssetBay]] = None, ) -> list[SwitchableAsset]: """Get the list of switchable assets from the DataFrame. @@ -137,11 +135,6 @@ def get_list_of_switchable_assets_from_df( station_branches: pd.DataFrame DataFrame with the switchable assets Note: datatype of columns is expected to be the same as in the pydantic model. - asset_bay_list: Optional[list[AssetBay]] - List of asset bays. - Note: The list is expected to have the same length as the station_branches. - asset_bay_dict: Optional[dict[str, AssetConnectionPath]] - Dictionary of asset bays with the asset grid_model_id as key. Returns ------- @@ -149,13 +142,6 @@ def get_list_of_switchable_assets_from_df( List of switchable assets. """ switchable_assets_dict = station_branches.to_dict(orient="records") - if asset_bay_list is not None: - for index, _ in enumerate(switchable_assets_dict): - switchable_assets_dict[index]["asset_bay_id"] = asset_bay_list[index].asset_bay_id - elif asset_bay_dict is not None: - for index, asset in enumerate(switchable_assets_dict): - if asset["grid_model_id"] in asset_bay_dict: - switchable_assets_dict[index]["asset_bay_id"] = asset_bay_dict[asset["grid_model_id"]].asset_bay_id switchable_assets_list = [SwitchableAsset(**switchable_asset) for switchable_asset in switchable_assets_dict] return switchable_assets_list @@ -466,7 +452,7 @@ def get_raw_stations_and_assets( ) assets = get_list_of_switchable_assets_from_df(station_elements) for asset in assets: - topology_assets.setdefault(asset.grid_model_id, asset.model_copy(update={"asset_bay_id": None})) + topology_assets.setdefault(asset.grid_model_id, asset.model_copy(deep=True)) station = RawStation( grid_model_id=bus_id, @@ -475,9 +461,10 @@ def get_raw_stations_and_assets( voltage_level=bus_info.nominal_v, busbars=get_list_of_busbars_from_df(station_buses), couplers=get_list_of_coupler_from_df(coupler_elements), - asset_ids=[asset.grid_model_id for asset in assets], - asset_terminals=asset_terminals, - asset_bay_ids=[None] * len(assets), + asset_connections=[ + StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) + for asset, asset_terminal in zip(assets, asset_terminals, strict=True) + ], asset_switching_table=switching_matrix, asset_connectivity=asset_connectivity, ) @@ -611,16 +598,17 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati switching_table[idx, asset_index] = True for asset in assets: - topology_assets.setdefault(asset.grid_model_id, asset.model_copy(update={"asset_bay_id": None})) + topology_assets.setdefault(asset.grid_model_id, asset.model_copy(deep=True)) station = RawStation( grid_model_id=bus_id, name=bus_row.name, busbars=busbars, couplers=couplers, - asset_ids=[asset.grid_model_id for asset in assets], - asset_terminals=asset_terminals, - asset_bay_ids=[None] * len(assets), + asset_connections=[ + StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) + for asset, asset_terminal in zip(assets, asset_terminals, strict=True) + ], asset_switching_table=switching_table, ) stations.append(station) @@ -667,11 +655,12 @@ def assert_station_in_network( # noqa: C901 bus_breaker_topo = net.get_bus_breaker_topology(buses_df.loc[station.grid_model_id]["voltage_level_id"]) - for asset in station.assets: + for asset_connection in station.asset_connections: + asset = asset_connection.asset if asset.grid_model_id not in bus_breaker_topo.elements.index: raise ValueError(f"Asset {asset.grid_model_id} not found in the station elements: {bus_breaker_topo.elements}") - if assets_strict and len(bus_breaker_topo.elements) != len(station.assets): - raise ValueError(f"Asset count mismatch: {len(bus_breaker_topo.elements)} != {len(station.assets)}") + if assets_strict and len(bus_breaker_topo.elements) != len(station.asset_connections): + raise ValueError(f"Asset count mismatch: {len(bus_breaker_topo.elements)} != {len(station.asset_connections)}") for busbar in station.busbars: if busbar.grid_model_id not in bus_breaker_topo.buses.index: diff --git a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py index 82c39fe63..5025541af 100644 --- a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py @@ -478,16 +478,21 @@ def test_assert_station_in_network_asset(case14_data_with_asset_topo: tuple[Path net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) # Add a switchable asset to the station that is not in the grid - station = topology.materialize_stations()[0].model_copy( + base_station = topology.materialize_stations()[0] + station = base_station.model_copy( update={ - "assets": topology.materialize_stations()[0].assets + "asset_connections": base_station.asset_connections + [ - topology.materialize_stations()[0].assets[0].model_copy(update={"grid_model_id": "hugawuga"}), + base_station.asset_connections[0].model_copy( + update={ + "asset": base_station.asset_connections[0].asset.model_copy(update={"grid_model_id": "hugawuga"}) + } + ), ], "asset_switching_table": np.concatenate( [ - topology.materialize_stations()[0].asset_switching_table, - topology.materialize_stations()[0].asset_switching_table[:, 0:1], + base_station.asset_switching_table, + base_station.asset_switching_table[:, 0:1], ], axis=1, ), @@ -497,10 +502,10 @@ def test_assert_station_in_network_asset(case14_data_with_asset_topo: tuple[Path assert_station_in_network(net, station) # Remove a switchable asset from the station - station = topology.materialize_stations()[0].model_copy( + station = base_station.model_copy( update={ - "assets": topology.materialize_stations()[0].assets[:-1], - "asset_switching_table": topology.materialize_stations()[0].asset_switching_table[:, :-1], + "asset_connections": base_station.asset_connections[:-1], + "asset_switching_table": base_station.asset_switching_table[:, :-1], } ) # Should pass without strict @@ -519,7 +524,7 @@ def test_convert_bus_breaker_stations_to_asset_topo() -> None: for station in stations: assert len(station.busbars) == 2 assert len(station.couplers) == 1 - for asset_id in station.asset_ids: + for asset_id in [asset_connection.asset_id for asset_connection in station.asset_connections]: assert any(asset.grid_model_id == asset_id for asset in assets) for asset in assets: diff --git a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py index 73c8a9264..030607d81 100644 --- a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py +++ b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py @@ -297,7 +297,7 @@ def get_changes_from_switching_table( """ switching_table = station.asset_switching_table busbar_name_list = [busbar.grid_model_id for busbar in station.busbars] - asset_list = station.assets + asset_list = [asset_connection.asset for asset_connection in station.asset_connections] change_list = [] # TODO: make a dataclass for this (code style) # loop over assets -> by column for asset_index, asset_in_table in enumerate(switching_table.T): 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 8025774aa..6bd2c76a2 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 @@ -52,7 +52,12 @@ get_voltage_level_with_region, ) from toop_engine_importer.pypowsybl_import.powsybl_masks import NetworkMasks -from toop_engine_interfaces.asset_topology import MaterializedStation, Topology, topology_parts_from_materialized_station +from toop_engine_interfaces.asset_topology import ( + MaterializedAssetConnection, + MaterializedStation, + Topology, + topology_parts_from_materialized_station, +) from toop_engine_interfaces.messages.preprocess.preprocess_commands import CgmesImporterParameters logger = structlog.get_logger(__name__) @@ -385,9 +390,13 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati asset_switching_table = remove_double_connections(asset_switching_table, substation_id=substation_id) busbars = get_list_of_busbars_from_df(busbar_df) couplers = get_list_of_coupler_from_df(coupler_df) - assets = get_list_of_switchable_assets_from_df(station_branches=switchable_assets_df, asset_bay_dict=asset_bay_dict) + assets = get_list_of_switchable_assets_from_df(station_branches=switchable_assets_df) remove_suffix_from_switchable_assets(assets) asset_bays_by_id = {asset_bay.asset_bay_id: asset_bay for asset_bay in asset_bay_dict.values()} + station_asset_bay_ids = [ + asset_bay_dict[asset_grid_model_id].asset_bay_id if asset_grid_model_id in asset_bay_dict else None + for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list() + ] station = MaterializedStation( grid_model_id=bus_id, @@ -396,8 +405,13 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati voltage_level=int(station_info.nominal_v), busbars=busbars, couplers=couplers, - assets=assets, - asset_bays=[asset_bays_by_id.get(asset.asset_bay_id) for asset in assets], + asset_connections=[ + MaterializedAssetConnection( + asset=asset, + asset_bay=asset_bays_by_id.get(asset_bay_id) if asset_bay_id is not None else None, + ) + for asset, asset_bay_id in zip(assets, station_asset_bay_ids, strict=True) + ], asset_switching_table=asset_switching_table, asset_connectivity=asset_connectivity, busbar_switching_table=busbar_switching_table, diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index 86c2434d6..65d7ce27d 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -33,6 +33,7 @@ ) from toop_engine_interfaces.asset_topology import ( AssetBay, + MaterializedAssetConnection, MaterializedStation, Topology, build_asset_bay_id, @@ -476,9 +477,12 @@ def get_station_from_id( voltage_level=voltage_level_float, busbars=busbar_list, couplers=coupler_list, - assets=switchable_assets_list, - asset_terminals=asset_terminals, - asset_bays=asset_connection_path, + asset_connections=[ + MaterializedAssetConnection(asset=asset, terminal=asset_terminal, asset_bay=asset_bay) + for asset, asset_terminal, asset_bay in zip( + switchable_assets_list, asset_terminals, asset_connection_path, strict=True + ) + ], asset_switching_table=switching_matrix, ) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py index a197922c9..47f2414ff 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py @@ -276,16 +276,17 @@ def validate_asset_topology(net: pp.pandapowerNet, topology_model: Topology) -> connection_dict = pp.toolbox.get_connected_elements_dict(net, [s_id]) del connection_dict["bus"] len_connection = len([element for key in connection_dict for element in connection_dict[key]]) - if len_connection != len(station.assets): + if len_connection != len(station.asset_connections): logger.warning( - f"Station {s_id} has {len(station.assets)} assets but only " + f"Station {s_id} has {len(station.asset_connections)} assets but only " + f"{len_connection} connections in the network", **connection_dict, ) - for asset in station.assets: - logger.warning(f"Station {s_id} with assets: {asset}", asset=asset) + for asset_connection in station.asset_connections: + logger.warning(f"Station {s_id} with assets: {asset_connection.asset}", asset=asset_connection.asset) raise ValueError( - f"Station {s_id} has {len(station.assets)} assets but only " + f"{len_connection} connections in the network" + f"Station {s_id} has {len(station.asset_connections)} assets but only " + + f"{len_connection} connections in the network" ) 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 4a6f532eb..c07f17599 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 @@ -159,9 +159,9 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assert not couplers[0].open assert couplers[0].in_service - assets = res.assets - asset_bays = res.asset_bays - asset_terminals = res.asset_terminals + assets = [asset_connection.asset for asset_connection in res.asset_connections] + asset_bays = [asset_connection.asset_bay for asset_connection in res.asset_connections] + asset_terminals = [asset_connection.terminal for asset_connection in res.asset_connections] assert len(assets) == 5 assert asset_terminals == [None] * len(assets) assert assets[0].grid_model_id == "L3" @@ -234,9 +234,15 @@ def test_get_station_edge_cases_one_bay_two_assets(asset_topo_edge_cases_node_br station_info = {"name": "Station_ID", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL1"} station_info = SubstationInformation(**station_info) res = get_station(net, "VL1_1", station_info) - load_assets = [asset for asset in res.assets if "load" in asset.grid_model_id] + load_assets = [ + asset_connection.asset + for asset_connection in res.asset_connections + if "load" in asset_connection.asset.grid_model_id + ] load_asset_bays = [ - asset_bay for asset, asset_bay in zip(res.assets, res.asset_bays, strict=True) if "load" in asset.grid_model_id + asset_connection.asset_bay + for asset_connection in res.asset_connections + if "load" in asset_connection.asset.grid_model_id ] assert len(load_assets) == 2, "Expected two loads" assert load_asset_bays[0] == load_asset_bays[1], "Both loads should be in the same asset bay" @@ -267,10 +273,10 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): coupler.busbar_from_id in busbar_int_ids and coupler.busbar_to_id in busbar_int_ids for coupler in res.couplers ) assert len([coupler for coupler in res.couplers if coupler.type == "BREAKER"]) > 0 - assert res.asset_switching_table.shape == (len(res.busbars), len(res.assets)) - assert res.asset_connectivity.shape == (len(res.busbars), len(res.assets)) + assert res.asset_switching_table.shape == (len(res.busbars), len(res.asset_connections)) + assert res.asset_connectivity.shape == (len(res.busbars), len(res.asset_connections)) - for asset_bay in res.asset_bays: + for asset_bay in [asset_connection.asset_bay for asset_connection in res.asset_connections]: if asset_bay is None: continue assert set(asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids @@ -304,11 +310,11 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): coupler.busbar_from_id in busbar_int_ids and coupler.busbar_to_id in busbar_int_ids for coupler in res.couplers ) - assert res.asset_switching_table.shape == (len(res.busbars), len(res.assets)) - assert res.asset_connectivity.shape == (len(res.busbars), len(res.assets)) + assert res.asset_switching_table.shape == (len(res.busbars), len(res.asset_connections)) + assert res.asset_connectivity.shape == (len(res.busbars), len(res.asset_connections)) assert res.asset_connectivity.any(axis=0).all() - for asset_bay in res.asset_bays: + for asset_bay in [asset_connection.asset_bay for asset_connection in res.asset_connections]: if asset_bay is None: continue assert set(asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index fbccc6ae7..7734af476 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -28,6 +28,7 @@ from toop_engine_interfaces.asset_topology import ( Busbar, RawStation, + StationAssetConnection, SwitchableAsset, Topology, ) @@ -71,9 +72,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow Busbar(int_id=1, grid_model_id="bb1", bus_branch_bus_id=buses.index[1]), ], couplers=[], - asset_ids=["asset0"], - asset_terminals=[None], - asset_bay_ids=[None], + asset_connections=[StationAssetConnection(asset_id="asset0", terminal=None, asset_bay_id=None)], asset_switching_table=np.array([[True], [False]]), ) regular_station = RawStation( @@ -81,9 +80,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow name="regular", busbars=[Busbar(int_id=0, grid_model_id="bb2", bus_branch_bus_id=buses.index[1])], couplers=[], - asset_ids=["asset1"], - asset_terminals=[None], - asset_bay_ids=[None], + asset_connections=[StationAssetConnection(asset_id="asset1", terminal=None, asset_bay_id=None)], asset_switching_table=np.array([[True]]), ) topology_model = Topology( diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py index 32996829a..efa42745f 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py @@ -255,9 +255,6 @@ class SwitchableAsset(BaseModel): ignored. This shall not be used for elements intentionally disconnected, instead set all zeros in the switching table.""" - asset_bay_id: Optional[str] = None - """Topology-scoped identifier for the asset bay associated with this asset.""" - def is_branch(self) -> Optional[bool]: """Return True if the asset is a branch. @@ -273,6 +270,32 @@ def is_branch(self) -> Optional[bool]: return self.type in get_args(AssetBranchType) +class StationAssetConnection(BaseModel): + """Station-local association between a switching-table column and a topology asset.""" + + asset_id: str + """Grid model id of the topology-owned asset referenced by this station-local column.""" + + terminal: Optional[BranchEnd] = None + """Optional branch terminal metadata for this station-local asset occurrence.""" + + asset_bay_id: Optional[str] = None + """Optional topology-scoped asset bay identifier for this station-local asset occurrence.""" + + +class MaterializedAssetConnection(BaseModel): + """Station-local association between a switching-table column and a materialized asset payload.""" + + asset: SwitchableAsset + """Station-local asset payload aligned with one switching-table column.""" + + terminal: Optional[BranchEnd] = None + """Optional branch terminal metadata for this station-local asset occurrence.""" + + asset_bay: Optional[AssetBay] = None + """Optional station-local asset bay payload for this station-local asset occurrence.""" + + class AssetSetpoint(BaseModel): """Asset data describing a single asset with a setpoint. @@ -521,61 +544,17 @@ class MaterializedStation(_StationStructure): station view; they are not intended to define a topology-owned canonical asset list. """ - assets: list[SwitchableAsset] - """The list of assets at the station. - - The order of this list is the same order as the assets in the asset_switching_table. - This list is station-local and switching-table aligned. Topology-owned canonical assets are - stored on Topology.assets instead. - """ - - asset_terminals: list[Optional[BranchEnd]] = Field(default_factory=list) - """Station-local branch terminals aligned with ``assets``. - - Each entry describes which terminal of the asset is connected in this station view. - """ - - asset_bays: list[Optional[AssetBay]] = Field(default_factory=list) - """Station-local asset bay payloads aligned with ``assets``. - - Each entry corresponds to the asset at the same index. ``None`` means the asset has no bay. - """ + asset_connections: list[MaterializedAssetConnection] + """Station-local asset payloads aligned with the switching tables.""" @model_validator(mode="after") def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": """Check if switching-table-aligned station-local assets match the matrix shapes.""" - if len(self.asset_terminals) == 0: - self.asset_terminals = [None] * len(self.assets) - elif len(self.asset_terminals) != len(self.assets): - raise ValueError( - f"asset_terminals length {len(self.asset_terminals)} does not match assets length {len(self.assets)}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - if len(self.asset_bays) == 0: - self.asset_bays = [None] * len(self.assets) - elif len(self.asset_bays) != len(self.assets): - raise ValueError( - f"asset_bays length {len(self.asset_bays)} does not match assets length {len(self.assets)}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - for index, (asset, asset_bay) in enumerate(zip(self.assets, self.asset_bays, strict=True)): - if asset_bay is None: - continue - if asset.asset_bay_id is None: - asset.asset_bay_id = asset_bay.asset_bay_id - elif asset.asset_bay_id != asset_bay.asset_bay_id: - raise ValueError( - f"asset_bay_id mismatch for asset index {index}: {asset.asset_bay_id} != {asset_bay.asset_bay_id}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - _validate_station_switching_tables( station_grid_model_id=self.grid_model_id, station_name=self.name, busbar_count=len(self.busbars), - asset_count=len(self.assets), + asset_count=len(self.asset_connections), asset_switching_table=self.asset_switching_table, asset_connectivity=self.asset_connectivity, ) @@ -591,7 +570,9 @@ def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": """Check if the asset bay bus is in busbars.""" busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] - for asset, asset_bay in zip(self.assets, self.asset_bays, strict=True): + for asset_connection in self.asset_connections: + asset = asset_connection.asset + asset_bay = asset_connection.asset_bay if asset_bay is not None: for busbar_id in asset_bay.sr_switch_grid_model_id.keys(): if busbar_id not in busbar_grid_model_id: @@ -622,9 +603,7 @@ def __eq__(self, other: object) -> bool: and self.region == other.region and self.busbars == other.busbars and self.couplers == other.couplers - and self.assets == other.assets - and self.asset_terminals == other.asset_terminals - and self.asset_bays == other.asset_bays + and self.asset_connections == other.asset_connections and np.array_equal(self.asset_switching_table, other.asset_switching_table) and ( np.array_equal(self.asset_connectivity, other.asset_connectivity) @@ -642,37 +621,35 @@ class RawStation(_StationStructure): SwitchableAsset payloads. """ - asset_ids: list[str] - """Asset grid model ids aligned with the switching tables. - - These are station-local references into Topology.assets. - """ + asset_connections: list[StationAssetConnection] + """Station-local asset references aligned with the switching tables.""" - asset_terminals: list[Optional[BranchEnd]] - """Station-local terminals aligned with the switching tables.""" + def with_asset_terminals(self, asset_terminals: list[Optional[BranchEnd]]) -> "RawStation": + """Return a copy with updated station-local branch terminals.""" + if len(asset_terminals) != len(self.asset_connections): + raise ValueError( + f"asset_terminals length {len(asset_terminals)} does not match asset_connections length " + f"{len(self.asset_connections)}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) - asset_bay_ids: list[Optional[str]] - """Topology-scoped asset bay identifiers aligned with the switching tables.""" + return self.model_copy( + update={ + "asset_connections": [ + asset_connection.model_copy(update={"terminal": asset_terminal}) + for asset_connection, asset_terminal in zip(self.asset_connections, asset_terminals, strict=True) + ] + } + ) @model_validator(mode="after") def check_asset_reference_alignment(self: "RawStation") -> "RawStation": """Check if station-local asset reference arrays are aligned.""" - if len(self.asset_terminals) != len(self.asset_ids): - raise ValueError( - f"asset_terminals length {len(self.asset_terminals)} " - f"does not match asset_ids length {len(self.asset_ids)}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - if len(self.asset_bay_ids) != len(self.asset_ids): - raise ValueError( - f"asset_bay_ids length {len(self.asset_bay_ids)} does not match asset_ids length {len(self.asset_ids)}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) _validate_station_switching_tables( station_grid_model_id=self.grid_model_id, station_name=self.name, busbar_count=len(self.busbars), - asset_count=len(self.asset_ids), + asset_count=len(self.asset_connections), asset_switching_table=self.asset_switching_table, asset_connectivity=self.asset_connectivity, ) @@ -696,9 +673,7 @@ def __eq__(self, other: object) -> bool: and self.voltage_level == other.voltage_level and self.busbars == other.busbars and self.couplers == other.couplers - and self.asset_ids == other.asset_ids - and self.asset_terminals == other.asset_terminals - and self.asset_bay_ids == other.asset_bay_ids + and self.asset_connections == other.asset_connections and np.array_equal(self.asset_switching_table, other.asset_switching_table) and ( np.array_equal(self.asset_connectivity, other.asset_connectivity) @@ -782,13 +757,15 @@ def check_station_asset_references(self: "Topology") -> "Topology": asset_bay_ids = {asset_bay.asset_bay_id for asset_bay in self.asset_bays} for station in self.raw_stations: - for asset_id in station.asset_ids: + for asset_connection in station.asset_connections: + asset_id = asset_connection.asset_id if asset_id not in asset_ids: raise ValueError( f"Asset grid_model_id {asset_id} referenced by station " f"{station.grid_model_id} does not exist in topology assets" ) - for asset_bay_id in station.asset_bay_ids: + for asset_connection in station.asset_connections: + asset_bay_id = asset_connection.asset_bay_id if asset_bay_id is not None and asset_bay_id not in asset_bay_ids: raise ValueError( f"asset_bay_id {asset_bay_id} referenced by station " @@ -804,23 +781,15 @@ def materialize_stations(self) -> list[MaterializedStation]: materialized_stations: list[MaterializedStation] = [] for station in self.raw_stations: - station_assets: list[SwitchableAsset] = [] - for asset_id, _asset_terminal, asset_bay_id in zip( - station.asset_ids, station.asset_terminals, station.asset_bay_ids, strict=True - ): - asset = asset_map[asset_id] - station_assets.append( - asset.model_copy( - update={ - "asset_bay_id": asset_bay_id, - }, - deep=True, - ) - ) + station_assets = [ + asset_map[asset_connection.asset_id].model_copy(deep=True) for asset_connection in station.asset_connections + ] station_asset_bays = [ - asset_bay_map[asset_bay_id].model_copy(deep=True) if asset_bay_id is not None else None - for asset_bay_id in station.asset_bay_ids + asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) + if asset_connection.asset_bay_id is not None + else None + for asset_connection in station.asset_connections ] materialized_stations.append( @@ -832,9 +801,19 @@ def materialize_stations(self) -> list[MaterializedStation]: voltage_level=station.voltage_level, busbars=station.busbars, couplers=station.couplers, - assets=station_assets, - asset_terminals=station.asset_terminals, - asset_bays=station_asset_bays, + asset_connections=[ + MaterializedAssetConnection( + asset=asset, + terminal=asset_connection.terminal, + asset_bay=asset_bay, + ) + for asset, asset_connection, asset_bay in zip( + station_assets, + station.asset_connections, + station_asset_bays, + strict=True, + ) + ], asset_switching_table=station.asset_switching_table, asset_connectivity=station.asset_connectivity, model_log=station.model_log, @@ -859,7 +838,9 @@ def get_asset_bay_ids_for_asset(self, asset_grid_model_id: str) -> list[str]: asset_bay_ids: list[str] = [] seen_ids: set[str] = set() for station in self.raw_stations: - for asset_id, asset_bay_id in zip(station.asset_ids, station.asset_bay_ids, strict=True): + for asset_connection in station.asset_connections: + asset_id = asset_connection.asset_id + asset_bay_id = asset_connection.asset_bay_id if asset_id != asset_grid_model_id or asset_bay_id is None or asset_bay_id in seen_ids: continue seen_ids.add(asset_bay_id) @@ -923,27 +904,25 @@ def topology_parts_from_materialized_station( """ assets: list[SwitchableAsset] = [] asset_bays: list[AssetBay] = [] - asset_ids: list[str] = [] - asset_terminals = list(station.asset_terminals) - asset_bay_ids: list[Optional[str]] = [] - for asset, asset_bay in zip(station.assets, station.asset_bays, strict=True): + asset_connections: list[StationAssetConnection] = [] + for asset_connection in station.asset_connections: + asset = asset_connection.asset + asset_bay = asset_connection.asset_bay asset_id = asset.grid_model_id - asset_bay_id: Optional[str] = asset.asset_bay_id + asset_bay_id: Optional[str] = None if asset_bay is not None: asset_bay_id = asset_bay.asset_bay_id asset_bays.append(asset_bay.model_copy(deep=True)) - assets.append( - asset.model_copy( - update={ - "asset_bay_id": None, - }, - deep=True, + assets.append(asset.model_copy(deep=True)) + asset_connections.append( + StationAssetConnection( + asset_id=asset_id, + terminal=asset_connection.terminal, + asset_bay_id=asset_bay_id, ) ) - asset_ids.append(asset_id) - asset_bay_ids.append(asset_bay_id) return ( RawStation( @@ -954,9 +933,7 @@ def topology_parts_from_materialized_station( voltage_level=station.voltage_level, busbars=station.busbars, couplers=station.couplers, - asset_ids=asset_ids, - asset_terminals=asset_terminals, - asset_bay_ids=asset_bay_ids, + asset_connections=asset_connections, asset_switching_table=station.asset_switching_table, asset_connectivity=station.asset_connectivity, model_log=station.model_log, diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py index 999246028..03e06af49 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py @@ -144,14 +144,17 @@ def filter_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: } ) - asset_bays = [filter_sr_keys(asset_bay) for asset_bay in station.asset_bays] + asset_connections = [ + asset_connection.model_copy(update={"asset_bay": filter_sr_keys(asset_connection.asset_bay)}) + for asset_connection in station.asset_connections + ] # Create a new station object with the modified busbars, couplers, and asset switching table new_station = station.model_copy( update={ "busbars": busbars, "couplers": couplers, - "asset_bays": asset_bays, + "asset_connections": asset_connections, "asset_switching_table": asset_switching_table, "asset_connectivity": asset_connectivity, } @@ -172,22 +175,17 @@ def filter_out_of_service_assets(station: MaterializedStation) -> MaterializedSt Station The new station object with all out-of-service assets removed. """ - if all(asset.in_service for asset in station.assets): + if all(asset_connection.asset.in_service for asset_connection in station.asset_connections): return station - in_service_assets = [asset.in_service for asset in station.assets] + in_service_asset_connections = [ + asset_connection for asset_connection in station.asset_connections if asset_connection.asset.in_service + ] + in_service_assets = [asset_connection.asset.in_service for asset_connection in station.asset_connections] return station.model_copy( update={ - "assets": [asset for asset in station.assets if asset.in_service], - "asset_terminals": [ - branch_end - for branch_end, in_service in zip(station.asset_terminals, in_service_assets, strict=True) - if in_service - ], - "asset_bays": [ - asset_bay for asset_bay, in_service in zip(station.asset_bays, in_service_assets, strict=True) if in_service - ], + "asset_connections": in_service_asset_connections, "asset_switching_table": station.asset_switching_table[:, in_service_assets], "asset_connectivity": station.asset_connectivity[:, in_service_assets] if station.asset_connectivity is not None @@ -444,20 +442,25 @@ def filter_assets_by_type( list[SwitchableAsset] The list of removed assets. """ - asset_mask = [(asset.type in assets_allowed) or (allow_none_type and asset.type is None) for asset in station.assets] + asset_mask = [ + (asset_connection.asset.type in assets_allowed) or (allow_none_type and asset_connection.asset.type is None) + for asset_connection in station.asset_connections + ] if all(asset_mask): return station, [] - removed_assets = [asset for asset, mask in zip(station.assets, asset_mask, strict=True) if not mask] - kept_assets = [asset for asset, mask in zip(station.assets, asset_mask, strict=True) if mask] + removed_assets = [ + asset_connection.asset + for asset_connection, mask in zip(station.asset_connections, asset_mask, strict=True) + if not mask + ] + kept_asset_connections = [ + asset_connection for asset_connection, mask in zip(station.asset_connections, asset_mask, strict=True) if mask + ] new_station = station.model_copy( update={ - "assets": kept_assets, - "asset_terminals": [ - branch_end for branch_end, mask in zip(station.asset_terminals, asset_mask, strict=True) if mask - ], - "asset_bays": [asset_bay for asset_bay, mask in zip(station.asset_bays, asset_mask, strict=True) if mask], + "asset_connections": kept_asset_connections, "asset_switching_table": station.asset_switching_table[:, asset_mask], "asset_connectivity": station.asset_connectivity[:, asset_mask] if station.asset_connectivity is not None @@ -550,7 +553,7 @@ def fix_multi_connected_without_coupler( new_asset_switching_table[bus1_idx, asset_idx] = 0 diff.append( ( - station.assets[asset_idx], + station.asset_connections[asset_idx].asset, station.busbars[bus1_idx], station.busbars[bus2_idx], ) @@ -575,7 +578,7 @@ def has_transmission_line_switching(station: MaterializedStation) -> bool: bool True if the switching table contains transmission line switching, False otherwise. """ - in_service = np.array([asset.in_service for asset in station.assets]) + in_service = np.array([asset_connection.asset.in_service for asset_connection in station.asset_connections]) return np.any((station.asset_switching_table.sum(axis=0) == 0) & in_service).item() @@ -916,8 +919,10 @@ def map_busbars_and_assets( # Same for the switchable assets asset_mapping = {} - for i, asset in enumerate(original_station.assets): - for j, other in enumerate(new_station.assets): + for i, asset_connection in enumerate(original_station.asset_connections): + asset = asset_connection.asset + for j, other_connection in enumerate(new_station.asset_connections): + other = other_connection.asset if asset.grid_model_id == other.grid_model_id: asset_mapping[i] = j break @@ -973,16 +978,24 @@ def compare_stations( a_couplers = set(coupler.grid_model_id for coupler in a.couplers) b_couplers = set(coupler.grid_model_id for coupler in b.couplers) - a_assets = set(asset.grid_model_id for asset in a.assets) - b_assets = set(asset.grid_model_id for asset in b.assets) + a_assets = set(asset_connection.asset.grid_model_id for asset_connection in a.asset_connections) + b_assets = set(asset_connection.asset.grid_model_id for asset_connection in b.asset_connections) return ( [coupler for coupler in a.couplers if coupler.grid_model_id not in b_couplers], [coupler for coupler in b.couplers if coupler.grid_model_id not in a_couplers], [busbar for busbar in a.busbars if busbar.grid_model_id not in b_busbars], [busbar for busbar in b.busbars if busbar.grid_model_id not in a_busbars], - [asset for asset in a.assets if asset.grid_model_id not in b_assets], - [asset for asset in b.assets if asset.grid_model_id not in a_assets], + [ + asset_connection.asset + for asset_connection in a.asset_connections + if asset_connection.asset.grid_model_id not in b_assets + ], + [ + asset_connection.asset + for asset_connection in b.asset_connections + if asset_connection.asset.grid_model_id not in a_assets + ], ) @@ -1074,7 +1087,7 @@ def get_connected_assets( busbar_index : int The index of the busbar in the switching table. topology_assets : Optional[list[SwitchableAsset]] - Topology-owned assets used to resolve ``RawStation.asset_ids``. Required when ``station`` is a + Topology-owned assets used to resolve ``RawStation.asset_connections``. Required when ``station`` is a ``RawStation``. Returns @@ -1084,13 +1097,21 @@ def get_connected_assets( """ connected_asset_indices = np.nonzero(station.asset_switching_table[busbar_index])[0] if isinstance(station, MaterializedStation): - return [station.assets[i] for i in connected_asset_indices if station.assets[i].in_service] + return [ + station.asset_connections[i].asset + for i in connected_asset_indices + if station.asset_connections[i].asset.in_service + ] if topology_assets is None: raise ValueError("topology_assets must be provided when resolving connected assets for a RawStation") asset_map = {asset.grid_model_id: asset for asset in topology_assets} - return [asset_map[station.asset_ids[i]] for i in connected_asset_indices if asset_map[station.asset_ids[i]].in_service] + return [ + asset_map[station.asset_connections[i].asset_id] + for i in connected_asset_indices + if asset_map[station.asset_connections[i].asset_id].in_service + ] def accumulate_diffs( @@ -1151,9 +1172,9 @@ def station_diff( AppliedStation The realized station containing the target station and the coupler, reassignment and disconnection diffs """ - assert [s.grid_model_id for s in start_station.assets] == [s.grid_model_id for s in target_station.assets], ( - "Assets do not match" - ) + assert [s.asset.grid_model_id for s in start_station.asset_connections] == [ + s.asset.grid_model_id for s in target_station.asset_connections + ], "Assets do not match" assert [b.grid_model_id for b in start_station.busbars] == [b.grid_model_id for b in target_station.busbars], ( "Busbars do not match" ) @@ -1163,7 +1184,7 @@ def station_diff( reassignment_diff = [] disconnection_diff = [] - for asset_index in range(len(start_station.assets)): + for asset_index in range(len(start_station.asset_connections)): target_disconnected = ~np.any(target_station.asset_switching_table[:, asset_index]) start_disconnected = ~np.any(start_station.asset_switching_table[:, asset_index]) @@ -1263,29 +1284,32 @@ def order_station_assets( list[str] A list of asset ids that were ignored, i.e. not present in the asset_ids list """ - new_assets = [] + new_asset_connections = [] not_found = [] old_positions = [] for asset_id in asset_ids: found = False - for pos, asset in enumerate(station.assets): + for pos, asset_connection in enumerate(station.asset_connections): + asset = asset_connection.asset if asset.grid_model_id == asset_id: - new_assets.append(asset) + new_asset_connections.append(asset_connection) old_positions.append(pos) found = True break if not found: not_found.append(asset_id) - ignored = [asset.grid_model_id for index, asset in enumerate(station.assets) if index not in old_positions] + ignored = [ + asset_connection.asset.grid_model_id + for index, asset_connection in enumerate(station.asset_connections) + if index not in old_positions + ] asset_switching_table = station.asset_switching_table[:, old_positions] asset_connectivity = station.asset_connectivity[:, old_positions] if station.asset_connectivity is not None else None station = station.model_copy( update={ - "assets": new_assets, - "asset_terminals": [station.asset_terminals[index] for index in old_positions], - "asset_bays": [station.asset_bays[index] for index in old_positions], + "asset_connections": new_asset_connections, "asset_switching_table": asset_switching_table, "asset_connectivity": asset_connectivity, } @@ -1433,14 +1457,17 @@ def _replace_int_id(coupler: BusbarCoupler) -> BusbarCoupler: new_busbars = [b for i, b in enumerate(station.busbars) if i != busbar_index_to_remove] new_couplers = [_replace_int_id(c) for c in station.couplers if c.grid_model_id != coupler_grid_model_id] - new_asset_bays = [_replace_sr_keys(asset_bay) for asset_bay in station.asset_bays] + new_asset_connections = [ + asset_connection.model_copy(update={"asset_bay": _replace_sr_keys(asset_connection.asset_bay)}) + for asset_connection in station.asset_connections + ] station = station.model_copy( update={ "busbars": new_busbars, "asset_switching_table": new_switching_table, "asset_connectivity": new_connectivity_table, - "asset_bays": new_asset_bays, + "asset_connections": new_asset_connections, "couplers": new_couplers, } ) diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index b1c4ac56d..c989b16fb 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -16,8 +16,10 @@ AssetBay, Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, RawStation, + StationAssetConnection, SwitchableAsset, Topology, build_asset_bay_id, @@ -35,7 +37,42 @@ ) +def materialized_asset_connections( + assets: list[SwitchableAsset], + terminals: list[str | None] | None = None, + asset_bays: list[AssetBay | None] | None = None, +) -> list[MaterializedAssetConnection]: + if terminals is None: + terminals = [None] * len(assets) + if asset_bays is None: + asset_bays = [None] * len(assets) + return [ + MaterializedAssetConnection(asset=asset, terminal=terminal, asset_bay=asset_bay) + for asset, terminal, asset_bay in zip(assets, terminals, asset_bays, strict=True) + ] + + +def raw_asset_connections( + asset_ids: list[str], + terminals: list[str | None] | None = None, + asset_bay_ids: list[str | None] | None = None, +) -> list[StationAssetConnection]: + if terminals is None: + terminals = [None] * len(asset_ids) + if asset_bay_ids is None: + asset_bay_ids = [None] * len(asset_ids) + return [ + StationAssetConnection(asset_id=asset_id, terminal=terminal, asset_bay_id=asset_bay_id) + for asset_id, terminal, asset_bay_id in zip(asset_ids, terminals, asset_bay_ids, strict=True) + ] + + def test_station() -> None: + assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + ] station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), @@ -44,11 +81,7 @@ def test_station() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -69,11 +102,7 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]).T, grid_model_id="station1", ) @@ -93,11 +122,7 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -117,11 +142,7 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -141,11 +162,7 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -171,12 +188,14 @@ def test_station() -> None: grid_model_id="coupler2", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", in_service=False), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4", in_service=False), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2", in_service=False), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4", in_service=False), + ] + ), asset_switching_table=np.array( [ [True, False, True, True], @@ -201,11 +220,7 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -218,11 +233,7 @@ def test_station() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="bus_id1", ) @@ -263,11 +274,7 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="bus_id0", ) @@ -292,7 +299,7 @@ def test_station_connectivity_tables(): station = MaterializedStation( busbars=busbars, couplers=couplers, - assets=assets, + asset_connections=materialized_asset_connections(assets), asset_switching_table=asset_switching_table, asset_connectivity=asset_connectivity, grid_model_id=grid_model_id, @@ -306,7 +313,7 @@ def test_station_connectivity_tables(): station = MaterializedStation( busbars=busbars, couplers=couplers, - assets=assets, + asset_connections=materialized_asset_connections(assets), asset_switching_table=asset_switching_table, asset_connectivity=asset_connectivity, grid_model_id=grid_model_id, @@ -317,7 +324,7 @@ def test_station_connectivity_tables(): station = MaterializedStation( busbars=busbars, couplers=couplers, - assets=assets, + asset_connections=materialized_asset_connections(assets), asset_switching_table=asset_switching_table, asset_connectivity=asset_connectivity, grid_model_id=grid_model_id, @@ -333,9 +340,7 @@ def test_topology_station_is_split() -> None: Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), ], couplers=[], - asset_ids=["line1"], - asset_terminals=[None], - asset_bay_ids=[None], + asset_connections=raw_asset_connections(["line1"]), asset_switching_table=np.array([[True], [False]]), ) assert not station.is_split() @@ -367,12 +372,17 @@ def test_schema() -> None: assert schema is not None assert "busbars" in schema["properties"] assert "couplers" in schema["properties"] - assert "assets" in schema["properties"] + assert "asset_connections" in schema["properties"] assert "asset_switching_table" in schema["properties"] assert "grid_model_id" in schema["properties"] def test_serialize_station() -> None: + assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + ] station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), @@ -381,11 +391,7 @@ def test_serialize_station() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -397,6 +403,11 @@ def test_serialize_station() -> None: def test_load_asset_topology() -> None: + station1_assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + ] station1 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), @@ -405,15 +416,16 @@ def test_load_asset_topology() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - ], + asset_connections=materialized_asset_connections(station1_assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) + station2_assets = [ + SwitchableAsset(grid_model_id="line4"), + SwitchableAsset(grid_model_id="line5"), + SwitchableAsset(grid_model_id="line6"), + ] station2 = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar3"), @@ -422,11 +434,7 @@ def test_load_asset_topology() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler2"), ], - assets=[ - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - SwitchableAsset(grid_model_id="line6"), - ], + asset_connections=materialized_asset_connections(station2_assets), asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station2", ) @@ -448,6 +456,18 @@ def test_load_asset_topology() -> None: def test_topology_extracts_assets_and_materializes_stations() -> None: + assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="load1"), + ] + asset_bays = [ + AssetBay( + asset_bay_id="station1::line1::bay", + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ), + None, + ] station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), @@ -456,22 +476,7 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset( - grid_model_id="line1", - asset_bay_id="station1::line1::bay", - ), - SwitchableAsset(grid_model_id="load1"), - ], - asset_terminals=["from", None], - asset_bays=[ - AssetBay( - asset_bay_id="station1::line1::bay", - dv_switch_grid_model_id="dv1", - sr_switch_grid_model_id={"busbar1": "sr1"}, - ), - None, - ], + asset_connections=materialized_asset_connections(assets, ["from", None], asset_bays), asset_switching_table=np.array([[True, False], [False, True]]), grid_model_id="station1", ) @@ -490,9 +495,7 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: grid_model_id="station1", busbars=station.busbars, couplers=station.couplers, - asset_ids=["line1", "load1"], - asset_terminals=["from", None], - asset_bay_ids=["station1::line1::bay", None], + asset_connections=raw_asset_connections(["line1", "load1"], ["from", None], ["station1::line1::bay", None]), asset_switching_table=np.array([[True, False], [False, True]]), ) ] @@ -500,14 +503,7 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: assert [asset_bay.asset_bay_id for asset_bay in topology.asset_bays] == ["station1::line1::bay"] materialized_station = topology.materialize_stations()[0] - assert materialized_station == station.model_copy( - update={ - "assets": [ - station.assets[0].model_copy(update={"asset_bay_id": "station1::line1::bay"}), - station.assets[1], - ] - } - ) + assert materialized_station == station def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_two_station_views() -> None: @@ -518,16 +514,14 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw grid_model_id="station_from", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], - assets=[asset_from], - asset_terminals=["from"], + asset_connections=materialized_asset_connections([asset_from], ["from"]), asset_switching_table=np.array([[True]]), ) station_to = MaterializedStation( grid_model_id="station_to", busbars=[Busbar(int_id=1, grid_model_id="busbar2")], couplers=[], - assets=[asset_to], - asset_terminals=["to"], + asset_connections=materialized_asset_connections([asset_to], ["to"]), asset_switching_table=np.array([[True]]), ) @@ -541,10 +535,10 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw ) assert [asset.grid_model_id for asset in topology.assets] == ["line1"] - assert topology.raw_stations[0].asset_ids == ["line1"] - assert topology.raw_stations[0].asset_terminals == ["from"] - assert topology.raw_stations[1].asset_ids == ["line1"] - assert topology.raw_stations[1].asset_terminals == ["to"] + assert [connection.asset_id for connection in topology.raw_stations[0].asset_connections] == ["line1"] + assert [connection.terminal for connection in topology.raw_stations[0].asset_connections] == ["from"] + assert [connection.asset_id for connection in topology.raw_stations[1].asset_connections] == ["line1"] + assert [connection.terminal for connection in topology.raw_stations[1].asset_connections] == ["to"] assert topology.materialize_stations() == [station_from, station_to] @@ -553,42 +547,34 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ grid_model_id="station_from", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], - assets=[ - SwitchableAsset( - grid_model_id="line1", - type="line", - asset_bay_id=build_asset_bay_id("station_from", "line1"), - ) - ], - asset_terminals=["from"], - asset_bays=[ - AssetBay( - asset_bay_id=build_asset_bay_id("station_from", "line1"), - dv_switch_grid_model_id="dv_from", - sr_switch_grid_model_id={"busbar1": "sr_from"}, - ) - ], + asset_connections=materialized_asset_connections( + [SwitchableAsset(grid_model_id="line1", type="line")], + ["from"], + [ + AssetBay( + asset_bay_id=build_asset_bay_id("station_from", "line1"), + dv_switch_grid_model_id="dv_from", + sr_switch_grid_model_id={"busbar1": "sr_from"}, + ) + ], + ), asset_switching_table=np.array([[True]]), ) station_to = MaterializedStation( grid_model_id="station_to", busbars=[Busbar(int_id=1, grid_model_id="busbar2")], couplers=[], - assets=[ - SwitchableAsset( - grid_model_id="line1", - type="line", - asset_bay_id=build_asset_bay_id("station_to", "line1"), - ) - ], - asset_terminals=["to"], - asset_bays=[ - AssetBay( - asset_bay_id=build_asset_bay_id("station_to", "line1"), - dv_switch_grid_model_id="dv_to", - sr_switch_grid_model_id={"busbar2": "sr_to"}, - ) - ], + asset_connections=materialized_asset_connections( + [SwitchableAsset(grid_model_id="line1", type="line")], + ["to"], + [ + AssetBay( + asset_bay_id=build_asset_bay_id("station_to", "line1"), + dv_switch_grid_model_id="dv_to", + sr_switch_grid_model_id={"busbar2": "sr_to"}, + ) + ], + ), asset_switching_table=np.array([[True]]), ) @@ -605,8 +591,12 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ "station_from::line1::bay", "station_to::line1::bay", ] - assert topology.raw_stations[0].asset_bay_ids == ["station_from::line1::bay"] - assert topology.raw_stations[1].asset_bay_ids == ["station_to::line1::bay"] + assert [connection.asset_bay_id for connection in topology.raw_stations[0].asset_connections] == [ + "station_from::line1::bay" + ] + assert [connection.asset_bay_id for connection in topology.raw_stations[1].asset_connections] == [ + "station_to::line1::bay" + ] def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_occurrence() -> None: @@ -614,30 +604,25 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ grid_model_id="station1", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], - assets=[ - SwitchableAsset( - grid_model_id="line1", - type="line", - asset_bay_id=build_asset_bay_id("station1", "line1"), - ), - SwitchableAsset( - grid_model_id="line1", - type="line", - asset_bay_id=build_asset_bay_id("station1", "line1", 1), - ), - ], - asset_bays=[ - AssetBay( - asset_bay_id=build_asset_bay_id("station1", "line1"), - dv_switch_grid_model_id="dv1", - sr_switch_grid_model_id={"busbar1": "sr1"}, - ), - AssetBay( - asset_bay_id=build_asset_bay_id("station1", "line1", 1), - dv_switch_grid_model_id="dv2", - sr_switch_grid_model_id={"busbar1": "sr2"}, - ), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1", type="line"), + SwitchableAsset(grid_model_id="line1", type="line"), + ], + None, + [ + AssetBay( + asset_bay_id=build_asset_bay_id("station1", "line1"), + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ), + AssetBay( + asset_bay_id=build_asset_bay_id("station1", "line1", 1), + dv_switch_grid_model_id="dv2", + sr_switch_grid_model_id={"busbar1": "sr2"}, + ), + ], + ), asset_switching_table=np.array([[True, True]]), ) @@ -650,8 +635,11 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ stations=[station], ) - assert topology.raw_stations[0].asset_ids == ["line1", "line1"] - assert topology.raw_stations[0].asset_bay_ids == ["station1::line1::bay", "station1::line1::bay::1"] + assert [connection.asset_id for connection in topology.raw_stations[0].asset_connections] == ["line1", "line1"] + assert [connection.asset_bay_id for connection in topology.raw_stations[0].asset_connections] == [ + "station1::line1::bay", + "station1::line1::bay::1", + ] assert sorted(asset_bay.asset_bay_id for asset_bay in topology.asset_bays) == [ "station1::line1::bay", "station1::line1::bay::1", @@ -659,6 +647,12 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ def test_filter_out_of_service() -> None: + assets = [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2", in_service=False), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4", in_service=False), + ] station = MaterializedStation( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), @@ -669,12 +663,7 @@ def test_filter_out_of_service() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler2"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", in_service=False), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4", in_service=False), - ], + asset_connections=materialized_asset_connections(assets), asset_switching_table=np.array( [ [True, False, True, True], @@ -688,7 +677,7 @@ def test_filter_out_of_service() -> None: station = filter_out_of_service(station) assert len(station.busbars) == 2 assert len(station.couplers) == 1 - assert len(station.assets) == 2 + assert len(station.asset_connections) == 2 assert np.array_equal(station.asset_switching_table, np.array([[True, True], [False, False]])) @@ -703,12 +692,14 @@ def test_has_transmission_line_switching() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler2"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", in_service=False), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4", in_service=False), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2", in_service=False), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4", in_service=False), + ] + ), asset_switching_table=np.array( [ [True, False, True, False], @@ -731,12 +722,14 @@ def test_has_transmission_line_switching() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler2"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ] + ), asset_switching_table=np.array( [ [True, False, True, True], @@ -763,12 +756,14 @@ def test_filter_duplicate_couplers() -> None: BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler3"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler4"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ] + ), asset_switching_table=np.array( [ [True, False, True, True], @@ -803,12 +798,14 @@ def test_filter_disconnected_busbars() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ] + ), asset_switching_table=np.array( [ [True, False, True, True], @@ -822,7 +819,7 @@ def test_filter_disconnected_busbars() -> None: station, removed = filter_disconnected_busbars(station) assert len(station.busbars) == 2 assert len(station.couplers) == 1 - assert len(station.assets) == 4 + assert len(station.asset_connections) == 4 assert np.array_equal( station.asset_switching_table, np.array([[True, False, True, True], [False, True, False, False]]), @@ -841,12 +838,14 @@ def test_filter_disconnected_busbars_sort_by_asset_count() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ] + ), asset_switching_table=np.array( [ [False, False, False, False], @@ -861,7 +860,7 @@ def test_filter_disconnected_busbars_sort_by_asset_count() -> None: assert len(station.busbars) == 1 assert station.busbars[0].grid_model_id == "busbar3" assert len(station.couplers) == 0 - assert len(station.assets) == 4 + assert len(station.asset_connections) == 4 assert np.array_equal( station.asset_switching_table, np.array([[True, False, True, True]]), @@ -882,12 +881,14 @@ def test_select_one_for_multi_connected_assets() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=False, grid_model_id="coupler2"), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), # Connects busbar 1 and 3 - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), # Connects busbar 2 and 3 - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ] + ), asset_switching_table=np.array( [ [True, False, True, False], @@ -922,13 +923,15 @@ def test_filter_assets_by_type() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - assets=[ - SwitchableAsset(grid_model_id="line1", type="line"), - SwitchableAsset(grid_model_id="line2", type="line"), - SwitchableAsset(grid_model_id="gen1", type="gen"), - SwitchableAsset(grid_model_id="load1", type="load"), - SwitchableAsset(grid_model_id="line3", type=None), - ], + asset_connections=materialized_asset_connections( + [ + SwitchableAsset(grid_model_id="line1", type="line"), + SwitchableAsset(grid_model_id="line2", type="line"), + SwitchableAsset(grid_model_id="gen1", type="gen"), + SwitchableAsset(grid_model_id="load1", type="load"), + SwitchableAsset(grid_model_id="line3", type=None), + ] + ), asset_switching_table=np.array( [ [True, False, True, False, True], @@ -939,19 +942,19 @@ def test_filter_assets_by_type() -> None: ) station_filtered, removed = filter_assets_by_type(station, set(["line", "trafo"])) - assert len(station_filtered.assets) == 2 + assert len(station_filtered.asset_connections) == 2 assert len(removed) == 3 - assert station_filtered.assets[0].grid_model_id == "line1" - assert station_filtered.assets[1].grid_model_id == "line2" + assert station_filtered.asset_connections[0].asset.grid_model_id == "line1" + assert station_filtered.asset_connections[1].asset.grid_model_id == "line2" assert station_filtered.asset_switching_table.shape == (2, 2) station_filtered, removed = filter_assets_by_type(station, set(["line", "gen"]), allow_none_type=True) - assert len(station_filtered.assets) == 4 + assert len(station_filtered.asset_connections) == 4 assert len(removed) == 1 - assert station_filtered.assets[0].grid_model_id == "line1" - assert station_filtered.assets[1].grid_model_id == "line2" - assert station_filtered.assets[2].grid_model_id == "gen1" - assert station_filtered.assets[3].grid_model_id == "line3" + assert station_filtered.asset_connections[0].asset.grid_model_id == "line1" + assert station_filtered.asset_connections[1].asset.grid_model_id == "line2" + assert station_filtered.asset_connections[2].asset.grid_model_id == "gen1" + assert station_filtered.asset_connections[3].asset.grid_model_id == "line3" def test_asset_bay() -> None: @@ -1017,7 +1020,7 @@ def test_station_bay() -> None: ] assets = [ SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", asset_bay_id=path.asset_bay_id), + SwitchableAsset(grid_model_id="line2"), SwitchableAsset(grid_model_id="line3"), ] asset_switching_table = np.array([[True, False, True], [False, True, False]]) @@ -1027,8 +1030,7 @@ def test_station_bay() -> None: station = MaterializedStation( busbars=busbars, couplers=couplers, - assets=assets, - asset_bays=[None, path, None], + asset_connections=materialized_asset_connections(assets, asset_bays=[None, path, None]), asset_switching_table=asset_switching_table, grid_model_id=grid_model_id, ) @@ -1043,15 +1045,14 @@ def test_station_bay() -> None: ) assets = [ SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", asset_bay_id=path_error.asset_bay_id), + SwitchableAsset(grid_model_id="line2"), SwitchableAsset(grid_model_id="line3"), ] with pytest.raises(ValidationError, match="busbar_id busbar3 in asset line2 does not exist in busbars"): station = MaterializedStation( busbars=busbars, couplers=couplers, - assets=assets, - asset_bays=[None, path_error, None], + asset_connections=materialized_asset_connections(assets, asset_bays=[None, path_error, None]), asset_switching_table=asset_switching_table, grid_model_id=grid_model_id, ) diff --git a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py index 37c8e8176..c4de76076 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py +++ b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py @@ -12,8 +12,10 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, RawStation, + StationAssetConnection, SwitchableAsset, Topology, ) @@ -76,15 +78,40 @@ def build_raw_station( voltage_level=None, busbars=busbars, couplers=couplers, - asset_ids=asset_ids, - asset_terminals=asset_terminals if asset_terminals is not None else [None] * len(asset_ids), - asset_bay_ids=asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), + asset_connections=[ + StationAssetConnection(asset_id=asset_id, terminal=asset_terminal, asset_bay_id=asset_bay_id) + for asset_id, asset_terminal, asset_bay_id in zip( + asset_ids, + asset_terminals if asset_terminals is not None else [None] * len(asset_ids), + asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), + strict=True, + ) + ], asset_switching_table=asset_switching_table, asset_connectivity=None, model_log=None, ) +def build_materialized_station( + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + assets: list[SwitchableAsset], + asset_switching_table: np.ndarray, + asset_connectivity: np.ndarray | None = None, +) -> MaterializedStation: + """Build a materialized station using explicit asset connections.""" + return MaterializedStation( + grid_model_id=grid_model_id, + busbars=busbars, + couplers=couplers, + asset_connections=[MaterializedAssetConnection(asset=asset) for asset in assets], + asset_switching_table=asset_switching_table, + asset_connectivity=asset_connectivity, + ) + + def build_test_topology(topology_id: str, raw_stations: list[RawStation], assets: list[SwitchableAsset]) -> Topology: """Build a topology from raw stations and topology-owned assets. @@ -111,7 +138,7 @@ def build_test_topology(topology_id: str, raw_stations: list[RawStation], assets def test_merge_stations(): - station1 = MaterializedStation( + station1 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -127,7 +154,7 @@ def test_merge_stations(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = MaterializedStation( + station2 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -177,7 +204,7 @@ def test_merge_stations(): def test_merge_stations_append_behavior(): - station1 = MaterializedStation( + station1 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -193,7 +220,7 @@ def test_merge_stations_append_behavior(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = MaterializedStation( + station2 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -243,7 +270,7 @@ def test_merge_stations_append_behavior(): def test_merge_stations_raise_behavior(): - station1 = MaterializedStation( + station1 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -259,7 +286,7 @@ def test_merge_stations_raise_behavior(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = MaterializedStation( + station2 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -284,7 +311,7 @@ def test_merge_stations_raise_behavior(): def test_merge_stations_with_new_station_append(): - station1 = MaterializedStation( + station1 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -300,7 +327,7 @@ def test_merge_stations_with_new_station_append(): asset_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station2 = MaterializedStation( + station2 = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -330,7 +357,7 @@ def test_merge_stations_with_new_station_append(): def test_get_connected_assets(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -358,8 +385,8 @@ def test_get_connected_assets(): assert len(connected_assets_busbar_1) == 0 # Test with no assets in service - station.assets[0].in_service = False - station.assets[2].in_service = False + station.asset_connections[0].asset.in_service = False + station.asset_connections[2].asset.in_service = False connected_assets_busbar_0 = get_connected_assets(station, 0) assert len(connected_assets_busbar_0) == 0 @@ -394,7 +421,7 @@ def test_find_busbars_for_coupler(): def test_station_diff_no_changes(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -419,7 +446,7 @@ def test_station_diff_no_changes(): def test_station_diff_coupler_state_change(): - start_station = MaterializedStation( + start_station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -451,7 +478,7 @@ def test_station_diff_coupler_state_change(): def test_station_diff_asset_reassignment(): - start_station = MaterializedStation( + start_station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -481,7 +508,7 @@ def test_station_diff_asset_reassignment(): def test_station_diff_asset_disconnection(): - start_station = MaterializedStation( + start_station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -511,7 +538,7 @@ def test_station_diff_asset_disconnection(): def test_station_diff_unsupported_reconnection(): - start_station = MaterializedStation( + start_station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -548,7 +575,7 @@ def test_topology_diff() -> None: station1_couplers = [ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ] - start_station_1 = MaterializedStation( + start_station_1 = build_materialized_station( busbars=station1_busbars, couplers=station1_couplers, assets=station1_assets, @@ -576,7 +603,7 @@ def test_topology_diff() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=False, grid_model_id="coupler2"), ] - start_station_2 = MaterializedStation( + start_station_2 = build_materialized_station( busbars=station2_busbars, couplers=station2_couplers, assets=station2_assets, @@ -642,7 +669,7 @@ def test_topology_diff() -> None: def test_filter_out_of_service(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1", in_service=True), Busbar(int_id=2, grid_model_id="busbar2", in_service=False), @@ -666,14 +693,14 @@ def test_filter_out_of_service(): assert len(filtered_station.couplers) == 0 - assert len(filtered_station.assets) == 1 - assert filtered_station.assets[0].grid_model_id == "line1" + assert len(filtered_station.asset_connections) == 1 + assert filtered_station.asset_connections[0].asset.grid_model_id == "line1" assert filtered_station.asset_switching_table.shape == (1, 1) def test_filter_disconnected_busbars(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -701,7 +728,7 @@ def test_filter_disconnected_busbars(): def test_filter_disconnected_busbars_open_coupler(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -732,7 +759,7 @@ def test_filter_disconnected_busbars_open_coupler(): def test_reindex_busbars(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=10, grid_model_id="busbar1"), Busbar(int_id=20, grid_model_id="busbar2"), @@ -759,7 +786,7 @@ def test_reindex_busbars(): def test_has_transmission_line_switching(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -780,7 +807,7 @@ def test_has_transmission_line_switching(): def test_order_station_assets() -> None: - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -813,8 +840,8 @@ def test_order_station_assets() -> None: assert not_found == [] assert ignored == ["line2"] - assert len(ordered.assets) == len(desired_order) - assert [asset.grid_model_id for asset in ordered.assets] == desired_order + assert len(ordered.asset_connections) == len(desired_order) + assert [asset_connection.asset.grid_model_id for asset_connection in ordered.asset_connections] == desired_order assert np.array_equal( ordered.asset_switching_table, np.array( @@ -831,8 +858,8 @@ def test_order_station_assets() -> None: assert not_found == ["pink_unicorn"] assert ignored == ["line2"] - assert len(ordered.assets) == len(desired_order) - 1 - assert [asset.grid_model_id for asset in ordered.assets] == [ + assert len(ordered.asset_connections) == len(desired_order) - 1 + assert [asset_connection.asset.grid_model_id for asset_connection in ordered.asset_connections] == [ "line5", "line4", "line3", @@ -923,7 +950,7 @@ def test_order_topology() -> None: def test_fuse_coupler(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -997,7 +1024,7 @@ def test_fuse_coupler(): def test_fuse_all_couplers_with_type(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1076,7 +1103,7 @@ def test_fuse_all_couplers_with_type(): def test_filter_duplicate_couplers(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1102,7 +1129,7 @@ def test_filter_duplicate_couplers(): def test_filter_duplicate_couplers_with_type_hierarchy(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1146,7 +1173,7 @@ def test_filter_duplicate_couplers_with_type_hierarchy(): def test_filter_duplicate_couplers_no_duplicates(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1170,7 +1197,7 @@ def test_filter_duplicate_couplers_no_duplicates(): def test_filter_duplicate_couplers_with_unknown_type(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -1199,7 +1226,7 @@ def test_filter_duplicate_couplers_with_unknown_type(): def test_filter_duplicate_couplers_multiple_duplicates(): - station = MaterializedStation( + station = build_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index 05d1c0273..e02315165 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -15,8 +15,10 @@ from toop_engine_interfaces.asset_topology import ( Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, RawStation, + StationAssetConnection, SwitchableAsset, Topology, ) @@ -75,9 +77,15 @@ def build_raw_station( voltage_level=None, busbars=busbars, couplers=couplers, - asset_ids=asset_ids, - asset_terminals=asset_terminals if asset_terminals is not None else [None] * len(asset_ids), - asset_bay_ids=asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), + asset_connections=[ + StationAssetConnection(asset_id=asset_id, terminal=asset_terminal, asset_bay_id=asset_bay_id) + for asset_id, asset_terminal, asset_bay_id in zip( + asset_ids, + asset_terminals if asset_terminals is not None else [None] * len(asset_ids), + asset_bay_ids if asset_bay_ids is not None else [None] * len(asset_ids), + strict=True, + ) + ], asset_switching_table=asset_switching_table, asset_connectivity=None, model_log=None, @@ -271,7 +279,7 @@ def test_validate_actions_grouped_accepts_grouped_actions(): voltage_level=None, busbars=[], couplers=[], - assets=[], + asset_connections=[], asset_switching_table=np.zeros((0, 0), dtype=bool), asset_connectivity=None, model_log=None, @@ -292,7 +300,7 @@ def test_validate_actions_grouped_raises_for_non_grouped_actions(): voltage_level=None, busbars=[], couplers=[], - assets=[], + asset_connections=[], asset_switching_table=np.zeros((0, 0), dtype=bool), asset_connectivity=None, model_log=None, @@ -321,7 +329,6 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): type=None, name=None, in_service=True, - asset_bay_id=None, ) ] @@ -333,7 +340,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - assets=assets, + asset_connections=[MaterializedAssetConnection.model_construct(asset=assets[0], terminal=None, asset_bay=None)], asset_switching_table=np.zeros((1, 1), dtype=bool), asset_connectivity=None, model_log=None, @@ -341,7 +348,15 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): station_b = station_a.model_copy( update={ "grid_model_id": "station_b", - "assets": [station_a.assets[0].model_copy(update={"grid_model_id": "station_b_asset_0"})], + "asset_connections": [ + station_a.asset_connections[0].model_copy( + update={ + "asset": station_a.asset_connections[0].asset.model_copy( + update={"grid_model_id": "station_b_asset_0"} + ) + } + ) + ], } ) @@ -358,9 +373,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - asset_ids=["station_a_asset_0"], - asset_terminals=[None], - asset_bay_ids=[None], + asset_connections=[StationAssetConnection(asset_id="station_a_asset_0", terminal=None, asset_bay_id=None)], asset_switching_table=np.zeros((1, 1), dtype=bool), asset_connectivity=None, model_log=None, @@ -373,15 +386,13 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - asset_ids=["station_b_asset_0"], - asset_terminals=[None], - asset_bay_ids=[None], + asset_connections=[StationAssetConnection(asset_id="station_b_asset_0", terminal=None, asset_bay_id=None)], asset_switching_table=np.zeros((1, 1), dtype=bool), asset_connectivity=None, model_log=None, ), ], - assets=station_a.assets + station_b.assets, + assets=[station_a.asset_connections[0].asset, station_b.asset_connections[0].asset], asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -434,7 +445,6 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): type=None, name=None, in_service=True, - asset_bay_id=None, ) for asset_idx in range(n_assets) ] @@ -473,7 +483,9 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): voltage_level=None, busbars=busbars, couplers=starting_couplers, - assets=assets, + asset_connections=[ + MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + ], asset_switching_table=starting_switching_table, asset_connectivity=None, model_log=None, @@ -566,14 +578,12 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): type=None, name=None, in_service=True, - asset_bay_id=None, ), SwitchableAsset.model_construct( grid_model_id="station_save_load_asset_2", type=None, name=None, in_service=True, - asset_bay_id=None, ), ] asset_switching_table = np.array([[True, False], [False, True]], dtype=bool) @@ -605,7 +615,9 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): voltage_level=None, busbars=busbars, couplers=couplers, - assets=assets, + asset_connections=[ + MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + ], asset_switching_table=asset_switching_table, asset_connectivity=None, model_log=None, @@ -676,14 +688,12 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): type=None, name=None, in_service=True, - asset_bay_id=None, ), SwitchableAsset.model_construct( grid_model_id="asset2", type=None, name=None, in_service=True, - asset_bay_id=None, ), ] asset_switching_table = np.array([[True, False], [False, True]], dtype=bool) @@ -715,7 +725,9 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): voltage_level=None, busbars=busbars, couplers=couplers, - assets=assets, + asset_connections=[ + MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + ], asset_switching_table=asset_switching_table, asset_connectivity=None, model_log=None, diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index 05f970394..0b9ad62ff 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -17,6 +17,7 @@ BusbarCoupler, MaterializedStation, RawStation, + StationAssetConnection, SwitchableAsset, Topology, ) @@ -94,7 +95,6 @@ def _build_large_random_action_set( type=None, name=None, in_service=True, - asset_bay_id=None, ) for asset_idx in range(n_assets) ] @@ -109,9 +109,14 @@ def _build_large_random_action_set( voltage_level=None, busbars=busbars, couplers=couplers, - asset_ids=[asset.grid_model_id for asset in assets], - asset_terminals=[None for _ in assets], - asset_bay_ids=[asset.asset_bay_id for asset in assets], + asset_connections=[ + StationAssetConnection.model_construct( + asset_id=asset.grid_model_id, + terminal=None, + asset_bay_id=None, + ) + for asset in assets + ], asset_switching_table=asset_switching_table, asset_connectivity=None, model_log=None, From 543c93da347de6ad0bce13751001411debc0d1fc Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Fri, 19 Jun 2026 10:09:38 +0200 Subject: [PATCH 14/24] pslit injection and branches Signed-off-by: Leonard Hilfrich --- .../sensitive-files-whitelist.txt | 8 + .../toop_engine_dc_solver/example_classes.py | 20 +- .../toop_engine_dc_solver/example_grids.py | 42 +- .../export/asset_topology_to_dgs.py | 8 +- .../export/disconnection_switch_updates.py | 9 +- .../toop_engine_dc_solver/export/export.py | 3 +- .../export/station_switch_updates.py | 216 ++- .../jax/benchmarks/bench_postprocessing.py | 17 +- .../src/toop_engine_dc_solver/jax/inputs.py | 1 - .../apply_asset_topo_pandapower.py | 266 ++- .../postprocess/apply_asset_topo_powsybl.py | 654 +++++-- .../postprocess/postprocess_pandapower.py | 100 +- .../postprocess/postprocess_powsybl.py | 41 +- .../postprocess/realize_assignment.py | 28 +- .../preprocess/action_set.py | 8 +- .../preprocess/helpers/ptdf.py | 32 +- .../preprocess/network_data.py | 4 +- .../preprocess/preprocess_bb_outage.py | 49 +- .../preprocess_station_realisations.py | 11 +- .../preprocess/preprocess_switching.py | 65 +- packages/dc_solver_pkg/tests/conftest.py | 26 +- .../tests/deprecated/assignment.py | 2 +- .../export/test_asset_topology_to_dgs.py | 52 +- .../test_disconnection_switch_updates.py | 13 +- .../dc_solver_pkg/tests/export/test_export.py | 3 +- .../export/test_station_switch_updates.py | 43 +- .../test_grid_node_breaker/action_set.json | 843 ++++++++- .../action_set_diffs.hdf5 | Bin 0 -> 7568 bytes .../files/test_grid_node_breaker/grid.xiidm | 732 ++++---- .../importer_auxiliary_data.json | 528 +++--- .../initial_topology/asset_topology.json | 933 +++++----- .../grid.xiidm} | 0 .../loadflow_parameters.json | 1 + .../masks/busbar_for_nminus1.npy | Bin 0 -> 150 bytes .../test_grid_node_breaker/network_data.pkl | Bin 17463 -> 0 bytes .../nminus1_definition.json | 361 ++-- .../optimizer_snapshot/run_0/res.json | 98 ++ .../run_0/topology_0/ac_loadflow_results.csv | 73 + .../run_0/topology_0/ac_metrics.json | 40 + .../run_0/topology_0/dc_loadflow_results.csv | 73 + .../run_0/topology_0/modified_network.xiidm | 374 ++++ .../run_0/topology_0/orao_summary.json | 1 + .../run_0/topology_0/sld/VLevel2_sld.svg | 647 +++++++ .../run_0/unsplit_ac_metrics.json | 28 + .../static_information.hdf5 | Bin 20427 -> 21203 bytes .../static_information.zip | Bin 2196 -> 0 bytes .../static_information_stats.json | 27 + .../benchmarks/test_bench_postprocessing.py | 4 +- .../tests/jax/test_busbar_outage.py | 21 +- .../dc_solver_pkg/tests/numpy_reference.py | 202 +++ .../test_apply_asset_topo_pandapower.py | 19 +- .../test_apply_asset_topo_powsybl.py | 243 ++- .../preprocessing/test_loadflows_match.py | 140 +- .../tests/preprocessing/test_preprocess.py | 13 +- .../test_preprocess_bb_outage.py | 87 +- .../test_preprocess_switching.py | 90 +- .../preprocessing/test_realize_assignment.py | 144 +- .../powsybl/example_grids.py | 5 +- .../powsybl/powsybl_asset_topo.py | 365 +++- .../tests/powsybl/test_powsybl_asset_topo.py | 229 ++- .../exporter/asset_topology_to_ucte.py | 10 +- .../network_graph/data_classes.py | 2 +- .../network_graph/graph_to_asset_topo.py | 16 +- .../network_graph/powsybl_station_to_graph.py | 233 ++- .../pandapower_import/asset_topology.py | 125 +- .../pandapower_import/preprocessing.py | 17 +- .../exporter/test_asset_topology_to_ucte.py | 38 +- .../network_graph/test_graph_to_asset_topo.py | 64 +- .../test_powsybl_station_to_graph.py | 69 +- .../test_asset_topology_pandapower.py | 2 +- .../pypowsybl_import/test_preprocessing.py | 15 +- .../toop_engine_interfaces/asset_topology.py | 561 ++++-- .../asset_topology_helpers.py | 1506 +++++++++++------ .../stored_action_set.py | 97 +- .../tests/test_asset_topology.py | 606 ++++--- .../tests/test_asset_topology_helpers.py | 309 +++- .../tests/test_stored_action_set.py | 271 ++- .../test_stored_action_set_performance.py | 36 +- .../ac/optimizer.py | 6 +- .../ac/scoring_functions.py | 2 +- toop-engine-benchmark/assess_benchmarks.py | 4 +- 81 files changed, 8605 insertions(+), 3426 deletions(-) create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set_diffs.hdf5 rename packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/{test_ucte_powsybl_example.xiidm => original_gridfile/grid.xiidm} (100%) create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/masks/busbar_for_nminus1.npy delete mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/network_data.pkl create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/res.json create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_loadflow_results.csv create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_metrics.json create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/dc_loadflow_results.csv create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/modified_network.xiidm create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/orao_summary.json create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/sld/VLevel2_sld.svg create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/unsplit_ac_metrics.json delete mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information.zip create mode 100644 packages/dc_solver_pkg/tests/files/test_grid_node_breaker/static_information_stats.json diff --git a/.pre-commit-hooks/sensitive-files-whitelist.txt b/.pre-commit-hooks/sensitive-files-whitelist.txt index c4b6e77d1..ba65d602b 100644 --- a/.pre-commit-hooks/sensitive-files-whitelist.txt +++ b/.pre-commit-hooks/sensitive-files-whitelist.txt @@ -48,6 +48,14 @@ 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/static_information_stats.json +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/unsplit_ac_metrics.json +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/orao_summary.json +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/modified_network.xiidm +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_metrics.json +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/res.json +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json +packages/dc_solver_pkg/tests/files/test_grid_node_breaker/action_set_diffs.hdf5 # Grid helpers package test files packages/grid_helpers_pkg/tests/files/test_ucte_powsybl_example.uct diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py index 47d4f6c4f..af03bd09d 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py @@ -12,11 +12,11 @@ import numpy as np from toop_engine_interfaces.asset_topology import ( AssetBay, + BranchAsset, Busbar, BusbarCoupler, RawStation, StationAssetConnection, - SwitchableAsset, Topology, ) @@ -56,20 +56,24 @@ def get_basic_node_breaker_topology() -> Topology: in_service=True, ) ], - asset_connections=[ + branch_connections=[ StationAssetConnection(asset_id="L4", terminal=None, asset_bay_id="VL4_0::L4::bay"), StationAssetConnection(asset_id="L5", terminal=None, asset_bay_id="VL4_0::L5::bay"), StationAssetConnection(asset_id="L8", terminal=None, asset_bay_id="VL4_0::L8::bay"), ], - asset_switching_table=np.array([[False, False, False], [True, True, False]]), - asset_connectivity=np.array([[True, True, True], [True, True, True]]), + injection_connections=[], + branch_switching_table=np.array([[False, False, False], [True, True, False]], dtype=bool), + injection_switching_table=np.zeros((2, 0), dtype=bool), + branch_connectivity=np.array([[True, True, True], [True, True, True]], dtype=bool), + injection_connectivity=np.zeros((2, 0), dtype=bool), ) ], - assets=[ - SwitchableAsset(grid_model_id="L4", type="LINE", name="", in_service=True), - SwitchableAsset(grid_model_id="L5", type="LINE", name="", in_service=True), - SwitchableAsset(grid_model_id="L8", type="LINE", name="", in_service=True), + branch_assets=[ + BranchAsset(grid_model_id="L4", type="LINE", name="", in_service=True), + BranchAsset(grid_model_id="L5", type="LINE", name="", in_service=True), + BranchAsset(grid_model_id="L8", type="LINE", name="", in_service=True), ], + injection_assets=[], asset_bays=[ AssetBay( asset_bay_id="VL4_0::L4::bay", 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 f1cabce69..a641226ac 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 @@ -52,11 +52,12 @@ from toop_engine_grid_helpers.powsybl.powsybl_helpers import save_lf_params_to_fs from toop_engine_importer.pypowsybl_import import preprocessing from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, MaterializedAssetConnection, MaterializedStation, - SwitchableAsset, Topology, topology_from_materialized_stations, ) @@ -193,12 +194,11 @@ def random_station_info_backend( ): if branch_node == node_idx: switchable_assets.append( - SwitchableAsset( + BranchAsset( grid_model_id=branch_id, - type=branch_type, + asset_type=branch_type, name=branch_name, in_service=True, - branch_end="from", ) ) @@ -211,12 +211,11 @@ def random_station_info_backend( ): if branch_node == node_idx: switchable_assets.append( - SwitchableAsset( + BranchAsset( grid_model_id=branch_id, - type=branch_type, + asset_type=branch_type, name=branch_name, in_service=True, - branch_end="to", ) ) @@ -229,18 +228,26 @@ def random_station_info_backend( ): if injection_node == node_idx: switchable_assets.append( - SwitchableAsset( + InjectionAsset( grid_model_id=injection_id, - type=injection_type, + asset_type=injection_type, name=injection_name, in_service=True, ) ) - asset_switching_table = np.zeros((2, len(switchable_assets)), dtype=bool) - is_on_a = np.random.rand(len(switchable_assets)) > 0.5 - asset_switching_table[0, is_on_a] = True - asset_switching_table[1, ~is_on_a] = True + branch_assets = [asset for asset in switchable_assets if isinstance(asset, BranchAsset)] + injection_assets = [asset for asset in switchable_assets if isinstance(asset, InjectionAsset)] + + branch_switching_table = np.zeros((2, len(branch_assets)), dtype=bool) + branch_is_on_a = np.random.rand(len(branch_assets)) > 0.5 + branch_switching_table[0, branch_is_on_a] = True + branch_switching_table[1, ~branch_is_on_a] = True + + injection_switching_table = np.zeros((2, len(injection_assets)), dtype=bool) + injection_is_on_a = np.random.rand(len(injection_assets)) > 0.5 + injection_switching_table[0, injection_is_on_a] = True + injection_switching_table[1, ~injection_is_on_a] = True global_id = backend.get_node_ids()[node_idx] if pp_counters is not None: @@ -280,9 +287,12 @@ def random_station_info_backend( open=False, ), ], - asset_connections=[MaterializedAssetConnection(asset=asset) for asset in switchable_assets], - asset_switching_table=asset_switching_table, - asset_connectivity=np.ones_like(asset_switching_table, dtype=bool), + branch_connections=[MaterializedAssetConnection(asset=asset) for asset in branch_assets], + injection_connections=[MaterializedAssetConnection(asset=asset) for asset in injection_assets], + branch_switching_table=branch_switching_table, + injection_switching_table=injection_switching_table, + branch_connectivity=np.ones_like(branch_switching_table, dtype=bool), + injection_connectivity=np.ones_like(injection_switching_table, dtype=bool), ), pp_counters diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/asset_topology_to_dgs.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/asset_topology_to_dgs.py index 29f6886fd..63af34c64 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/asset_topology_to_dgs.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/asset_topology_to_dgs.py @@ -20,7 +20,7 @@ import pandas as pd import pandera as pa import pandera.typing as pat -from beartype.typing import Any, Optional, cast +from beartype.typing import Optional from toop_engine_dc_solver.export.dgs_v7_definitions import ( DGS_GENERAL_SHEET_CONTENT_FID, DGS_GENERAL_SHEET_CONTENT_FID_CIM, @@ -89,7 +89,7 @@ def switch_update_schema_to_dgs( dgs_df = dgs_df.astype({"FID(a:40)": str, "OP": str, "on_off": int}) if cim: dgs_df["FID(a:40)"] = "_" + dgs_df["FID(a:40)"] - return cast(pat.DataFrame[DgsElmCoupSchema], dgs_df) + return dgs_df @pa.check_types @@ -104,7 +104,7 @@ def get_dgs_general_schema( general_info = DGS_GENERAL_SHEET_CONTENT_FID df_general = pd.DataFrame(general_info) - return cast(pat.DataFrame[DgsGeneralSchema], df_general) + return df_general @pa.check_types @@ -169,7 +169,7 @@ def switch_dgs_schema_to_bytes_io( BytesIO object containing the xlsx file with the DGS information """ bytes_io = io.BytesIO() - with pd.ExcelWriter(cast(Any, bytes_io), engine="openpyxl") as writer: + with pd.ExcelWriter(bytes_io, engine="openpyxl") as writer: df_general.to_excel(writer, index=False, sheet_name="General") switch_dgs_schema.to_excel(writer, index=False, sheet_name=sheet_name) bytes_io.seek(0) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py index d349182f9..389aad4e0 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py @@ -11,7 +11,6 @@ import pandera as pa import pandera.typing as pat import structlog -from beartype.typing import cast from toop_engine_interfaces.asset_topology import AssetBay, MaterializedStation, Topology from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema @@ -40,14 +39,14 @@ def get_disconnected_asset_ids( Returns ------- dict[str, list[AssetBay]] - Mapping of grid element IDs to be disconnected to the switchable assets that can perform the disconnection. + Mapping of grid element IDs to be disconnected to the switchable asset bays that can perform the disconnection. Note that not all requested disconnections might be representable as switch updates, so this mapping might be incomplete. """ disconnection_map: dict[str, GridElement] = {disconnection.id: disconnection for disconnection in disconnections} disconnection_asset_map: dict[str, list[AssetBay]] = {disconnection.id: [] for disconnection in disconnections} for station in stations: - for asset_connection in station.asset_connections: + for asset_connection in [*station.branch_connections, *station.injection_connections]: asset = asset_connection.asset asset_bay = asset_connection.asset_bay if asset.grid_model_id in disconnection_map and asset_bay is not None: @@ -104,6 +103,4 @@ def get_changing_switches_from_disconnections( } ) - return cast( - pat.DataFrame[SwitchUpdateSchema], pd.DataFrame.from_records(switch_updates, columns=["grid_model_id", "open"]) - ) + return pd.DataFrame.from_records(switch_updates, columns=["grid_model_id", "open"]) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py index f8972ab8b..09d07279e 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py @@ -16,7 +16,6 @@ import pandera as pa import pandera.typing as pat import structlog -from beartype.typing import cast from toop_engine_dc_solver.export.disconnection_switch_updates import get_changing_switches_from_disconnections from toop_engine_dc_solver.export.station_switch_updates import ( get_changing_switches_from_changed_stations, @@ -162,7 +161,7 @@ def get_changing_switches_from_actions( if combined_switch_updates.empty: combined_switch_updates = get_empty_dataframe_from_model(SwitchUpdateSchema) combined_switch_updates = combined_switch_updates.astype({"grid_model_id": str, "open": bool}) - return cast(pat.DataFrame[SwitchUpdateSchema], combined_switch_updates) + return combined_switch_updates @pa.check_types diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py index ff7740c1f..1b2111e88 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py @@ -15,8 +15,6 @@ import pandas as pd import pandera as pa import pandera.typing as pat -from beartype.typing import cast -from jaxtyping import Bool from toop_engine_interfaces.asset_topology import MaterializedStation, Topology from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema @@ -121,77 +119,159 @@ def _get_coupler_switch_diffs( return diff_switches -def _get_asset_switch_diffs( +def _get_branch_switch_diffs( changed_station: MaterializedStation, starting_station: MaterializedStation, fail_on_disconnect: bool = False, ) -> list[dict[str, str | bool]]: - """Collect selector and breaker switch changes between two station states. + """Collect branch selector and breaker switch changes between two station states. Parameters ---------- - changed_station : Station - Station describing the target asset-to-busbar assignments. - starting_station : Station - Station describing the reference asset-to-busbar assignments. The station assets must be - in the same order as ``changed_station``. This is the ordering contract provided by - ``ActionSet.simplified_starting_topology``. - fail_on_disconnect: bool - Fundamentally, the stations should never disconnect an element. If this is detected, we can either raise - or open the breaker. If fail_on_disconnect is true, a ValueError will be raised + changed_station : MaterializedStation + Station describing the target branch-to-busbar assignments. + starting_station : MaterializedStation + Station describing the reference branch assignments. The branch connection + array must stay in the same order as ``changed_station``. + fail_on_disconnect : bool, default=False + Whether to raise when a changed branch becomes fully disconnected instead + of emitting a breaker-opening update. Returns ------- list[dict[str, str | bool]] - Switch update records for selector and breaker switches whose state changes. + Switch update records derived from ``branch_connections`` and + ``branch_switching_table``. A changed branch column can produce selector + switch updates for busbar reassignments and, when the branch becomes + fully disconnected, one breaker update via the asset-bay disconnecting + switch. Raises ------ ValueError - If the station switching tables are structurally incompatible or asset order does not match. + If the branch switching tables are structurally incompatible, the branch + order does not match, or a disconnect is detected while + ``fail_on_disconnect`` is true. """ - changed_switching_table = _get_station_switching_table(changed_station) - starting_switching_table = _get_station_switching_table(starting_station) - if changed_switching_table.shape != starting_switching_table.shape: + if changed_station.branch_switching_table.shape != starting_station.branch_switching_table.shape: raise ValueError( "Changed station asset switching table shape does not match starting topology for station " f"{changed_station.grid_model_id}." ) - changed_busbar_lookup = _get_busbar_lookup(changed_station) - changed_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in changed_station.asset_connections] - starting_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in starting_station.asset_connections] + changed_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in changed_station.branch_connections] + starting_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in starting_station.branch_connections] if changed_asset_ids != starting_asset_ids: raise ValueError( "Changed station assets are not ordered like the starting topology for station " f"{changed_station.grid_model_id}. Use ActionSet.simplified_starting_topology as input." ) - switching_xor = np.logical_xor(starting_switching_table, changed_switching_table) + changed_busbar_lookup = _get_busbar_lookup(changed_station) + switching_xor = np.logical_xor(starting_station.branch_switching_table, changed_station.branch_switching_table) + diff_switches: list[dict[str, str | bool]] = [] + + for column, changed_asset_connection in enumerate(changed_station.branch_connections): + asset_bay = changed_asset_connection.asset_bay + if asset_bay is None: + continue + changed_switch_states = changed_station.branch_switching_table[:, column] + starting_switch_states = starting_station.branch_switching_table[:, column] + changed_rows = np.flatnonzero(switching_xor[:, column]) + changed_active = int(changed_switch_states.sum()) + starting_active = int(starting_switch_states.sum()) + + if changed_active == 0: + if starting_active > 0: + if fail_on_disconnect: + raise ValueError( + f"Station action in station {changed_station.grid_model_id} would disconnect " + f"asset {changed_asset_connection.asset.grid_model_id}." + ) + diff_switches.append({"grid_model_id": asset_bay.dv_switch_grid_model_id, "open": True}) + continue + + for row in changed_rows: + busbar_id = changed_busbar_lookup[int(row)] + switch_id = asset_bay.sr_switch_grid_model_id[busbar_id] + diff_switches.append({"grid_model_id": switch_id, "open": not bool(changed_switch_states[row])}) + + return diff_switches + + +def _get_injection_switch_diffs( + changed_station: MaterializedStation, + starting_station: MaterializedStation, + fail_on_disconnect: bool = False, +) -> list[dict[str, str | bool]]: + """Collect injection selector and breaker switch changes between two station states. + + Parameters + ---------- + changed_station : MaterializedStation + Station describing the target injection-to-busbar assignments. + starting_station : MaterializedStation + Station describing the reference injection assignments. The injection + connection array must stay in the same order as ``changed_station``. + fail_on_disconnect : bool, default=False + Whether to raise when a changed injection becomes fully disconnected + instead of emitting a breaker-opening update. + + Returns + ------- + list[dict[str, str | bool]] + Switch update records derived from ``injection_connections`` and + ``injection_switching_table``. This mirrors + ``_get_branch_switch_diffs`` for the injection-side station tables. + + Raises + ------ + ValueError + If the injection switching tables are structurally incompatible, the + injection order does not match, or a disconnect is detected while + ``fail_on_disconnect`` is true. + """ + if changed_station.injection_switching_table.shape != starting_station.injection_switching_table.shape: + raise ValueError( + "Changed station asset switching table shape does not match starting topology for station " + f"{changed_station.grid_model_id}." + ) + + changed_asset_ids = [asset_connection.asset.grid_model_id for asset_connection in changed_station.injection_connections] + starting_asset_ids = [ + asset_connection.asset.grid_model_id for asset_connection in starting_station.injection_connections + ] + if changed_asset_ids != starting_asset_ids: + raise ValueError( + "Changed station assets are not ordered like the starting topology for station " + f"{changed_station.grid_model_id}. Use ActionSet.simplified_starting_topology as input." + ) + changed_busbar_lookup = _get_busbar_lookup(changed_station) + switching_xor = np.logical_xor( + starting_station.injection_switching_table, + changed_station.injection_switching_table, + ) diff_switches: list[dict[str, str | bool]] = [] - for column, changed_asset_connection in enumerate(changed_station.asset_connections): - changed_asset = changed_asset_connection.asset + + for column, changed_asset_connection in enumerate(changed_station.injection_connections): asset_bay = changed_asset_connection.asset_bay if asset_bay is None: continue - changed_switch_states = changed_switching_table[:, column] - starting_switch_states = starting_switching_table[:, column] + changed_switch_states = changed_station.injection_switching_table[:, column] + starting_switch_states = starting_station.injection_switching_table[:, column] changed_rows = np.flatnonzero(switching_xor[:, column]) changed_active = int(changed_switch_states.sum()) starting_active = int(starting_switch_states.sum()) if changed_active == 0: - breaker_id = asset_bay.dv_switch_grid_model_id - # The asset was disconnected by disconnecting all entries in the switching table. This can only be represented - # through a breaker-open if there was at least one active busbar connection in the starting state. if starting_active > 0: if fail_on_disconnect: raise ValueError( f"Station action in station {changed_station.grid_model_id} would disconnect " - f"asset {changed_asset.grid_model_id}." + f"asset {changed_asset_connection.asset.grid_model_id}." ) - diff_switches.append({"grid_model_id": breaker_id, "open": True}) + diff_switches.append({"grid_model_id": asset_bay.dv_switch_grid_model_id, "open": True}) continue for row in changed_rows: @@ -202,6 +282,51 @@ def _get_asset_switch_diffs( return diff_switches +def _get_asset_switch_diffs( + changed_station: MaterializedStation, + starting_station: MaterializedStation, + fail_on_disconnect: bool = False, +) -> list[dict[str, str | bool]]: + """Collect selector and breaker switch changes between two station states. + + Parameters + ---------- + changed_station : MaterializedStation + Station describing the target branch/injection-to-busbar assignments. + starting_station : MaterializedStation + Station describing the reference branch/injection assignments. The branch and injection + connection arrays must each stay in the same order as ``changed_station``. This is the + ordering contract provided by ``ActionSet.simplified_starting_topology``. + fail_on_disconnect : bool, default=False + Fundamentally, the stations should never disconnect an element. If this is detected, the + helper can either raise or emit a breaker-opening update. If ``fail_on_disconnect`` is + true, a ``ValueError`` is raised instead. + + Returns + ------- + list[dict[str, str | bool]] + Switch update records for selector and breaker switches whose state changes across both the + branch and injection station tables. + + Raises + ------ + ValueError + If the station switching tables are structurally incompatible or asset order does not match. + """ + return [ + *_get_branch_switch_diffs( + changed_station=changed_station, + starting_station=starting_station, + fail_on_disconnect=fail_on_disconnect, + ), + *_get_injection_switch_diffs( + changed_station=changed_station, + starting_station=starting_station, + fail_on_disconnect=fail_on_disconnect, + ), + ] + + def _get_switch_updates_from_station_ids( changed_station_lookup: dict[str, MaterializedStation], starting_station_lookup: dict[str, MaterializedStation], @@ -241,23 +366,7 @@ def _get_switch_updates_from_station_ids( if diff_switch_df.empty: diff_switch_df = get_empty_dataframe_from_model(SwitchUpdateSchema) diff_switch_df = diff_switch_df.astype({"grid_model_id": str, "open": bool}) - return cast(pat.DataFrame[SwitchUpdateSchema], diff_switch_df) - - -def _get_station_switching_table(station: MaterializedStation) -> Bool[np.ndarray, " n_busbar n_asset"]: - """Return the station switching table as a boolean numpy array. - - Parameters - ---------- - station : Station - Station whose switching table should be normalized to a boolean ndarray. - - Returns - ------- - Bool[np.ndarray, " n_busbar n_asset"] - Boolean switching table indexed by busbar and asset. - """ - return np.asarray(station.asset_switching_table, dtype=bool) + return diff_switch_df @pa.check_types @@ -268,16 +377,19 @@ def get_changing_switches_from_changed_stations( """Get changed switches by comparing changed stations to the starting topology. This is intended for changed stations originating from ``ActionSet.local_actions`` where only - coupler open states and the asset switching table differ from the starting topology. + coupler open states and the split station switching tables differ from the starting topology. + In the split topology model that means ``branch_switching_table`` and + ``injection_switching_table`` are compared independently and then merged into one switch-update + table. Parameters ---------- - changed_stations : list[Station] + changed_stations : list[MaterializedStation] Stations describing the target state for the affected substations. starting_topology : Topology Starting topology containing the reference state for all stations. This is expected to be - ``ActionSet.simplified_starting_topology`` so that station asset ordering matches the - ordering used by ``ActionSet.local_actions``. + ``ActionSet.simplified_starting_topology`` so that both branch and injection connection + ordering match the ordering used by ``ActionSet.local_actions``. Returns diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/benchmarks/bench_postprocessing.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/benchmarks/bench_postprocessing.py index ab179682e..ab876f53b 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/benchmarks/bench_postprocessing.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/benchmarks/bench_postprocessing.py @@ -18,7 +18,7 @@ import jax import tyro -from beartype.typing import Literal, Optional, TypeAlias, cast +from beartype.typing import Literal, Optional, TypeAlias from pydantic import BaseModel, PositiveInt from toop_engine_dc_solver.jax.inputs import load_static_information from toop_engine_dc_solver.jax.topology_computations import random_topology @@ -168,15 +168,12 @@ def sample_topologies( ) -> ActionIndexComputations: """Sample topology actions using the persisted static information action set.""" static_information = load_static_information(data_folder / PREPROCESSING_PATHS["static_information_file_path"]) - return cast( - ActionIndexComputations, - random_topology( - rng_key=jax.random.PRNGKey(seed), - branch_action_set=static_information.dynamic_information.action_set, - limit_n_subs=n_substations_split, - batch_size=n_topologies, - topo_vect_format=False, - ), + return random_topology( + rng_key=jax.random.PRNGKey(seed), + branch_action_set=static_information.dynamic_information.action_set, + limit_n_subs=n_substations_split, + batch_size=n_topologies, + topo_vect_format=False, ) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/inputs.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/inputs.py index 550abce5b..70abd52e7 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/inputs.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/jax/inputs.py @@ -225,7 +225,6 @@ def validate_static_information( assert di.susceptance.shape == (n_branch,) assert jnp.all(di.susceptance != 0) - assert di.branches_to_fail.shape[0] <= n_branch assert di.action_set is not None diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py index 0803036d1..5ec1a277f 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py @@ -276,10 +276,166 @@ def delete_excess_switches( return deleted -def apply_station_assets( +def _apply_branch_assets( + net: pp.pandapowerNet, + station: MaterializedStation, + station_buses: Iterable[int], + station_bus_ids: list[int], +) -> tuple[list[int], list[tuple[int, int, bool]]]: + """Apply branch reassignment and disconnection updates for one station. + + Parameters + ---------- + net : pandapowerNet + The pandapower network to modify in place. + station : MaterializedStation + Station whose ``branch_connections`` and ``branch_switching_table`` + define the desired branch placements. + station_buses : Iterable[int] + All pandapower bus ids that are considered part of the station when + locating the currently connected bus for a branch. + station_bus_ids : list[int] + Bus ids aligned with ``station.busbars``. The branch switching-table row + indices are resolved through this ordering. + + Returns + ------- + tuple[list[int], list[tuple[int, int, bool]]] + A pair containing disconnected branch indices and branch reassignment + diffs. Each reassignment diff stores the branch index, the affected + station-bus index, and whether the branch was assigned or unassigned. + + Raises + ------ + NotImplementedError + If a branch is connected to multiple busbars at once. + """ + branch_disconnection_diff: list[int] = [] + branch_reassignment_diff: list[tuple[int, int, bool]] = [] + + for asset_index, asset_connection in enumerate(station.branch_connections): + asset = asset_connection.asset + pp_id, asset_type = parse_globally_unique_id(asset.grid_model_id) + pp_table = get_element_table(asset_type) + pp_id = int(pp_id) + target_buses: list[int] = np.flatnonzero(station.branch_switching_table[:, asset_index]).tolist() + + if len(target_buses) > 1: + raise NotImplementedError("Connecting an asset to multiple buses is not supported.") + + if len(target_buses) == 0: + net[pp_table].loc[pp_id, "in_service"] = False + branch_disconnection_diff.append(int(asset_index)) + continue + + target_bus_index = target_buses[0] + target_bus_id = station_bus_ids[target_bus_index] + previous_bus = reassign_asset_to_bus( + net=net, + asset_id=pp_id, + asset_table=pp_table, + target_bus_id=target_bus_id, + station_buses=station_buses, + ) + if previous_bus != target_bus_id: + branch_reassignment_diff.append((int(asset_index), int(target_bus_index), True)) + try: + previous_bus_index = station_bus_ids.index(previous_bus) + except ValueError: + previous_bus_index = -1 + logger.warning( + f"Asset {asset.grid_model_id} was reassigned from bus {previous_bus} which is not in the station." + ) + branch_reassignment_diff.append((int(asset_index), int(previous_bus_index), False)) + + return branch_disconnection_diff, branch_reassignment_diff + + +def _apply_injection_assets( net: pp.pandapowerNet, station: MaterializedStation, + station_buses: Iterable[int], + station_bus_ids: list[int], ) -> tuple[list[int], list[tuple[int, int, bool]]]: + """Apply injection reassignment and disconnection updates for one station. + + Parameters + ---------- + net : pandapowerNet + The pandapower network to modify in place. + station : MaterializedStation + Station whose ``injection_connections`` and + ``injection_switching_table`` define the desired injection placements. + station_buses : Iterable[int] + All pandapower bus ids that are considered part of the station when + locating the currently connected bus for an injection. + station_bus_ids : list[int] + Bus ids aligned with ``station.busbars``. The injection switching-table + row indices are resolved through this ordering. + + Returns + ------- + tuple[list[int], list[tuple[int, int, bool]]] + A pair containing disconnected injection indices and injection + reassignment diffs. Each reassignment diff stores the injection index, + the affected station-bus index, and whether the injection was assigned + or unassigned. + + Raises + ------ + NotImplementedError + If an injection is connected to multiple busbars at once. + """ + injection_disconnection_diff: list[int] = [] + injection_reassignment_diff: list[tuple[int, int, bool]] = [] + + for asset_index, asset_connection in enumerate(station.injection_connections): + asset = asset_connection.asset + pp_id, asset_type = parse_globally_unique_id(asset.grid_model_id) + pp_table = get_element_table(asset_type) + pp_id = int(pp_id) + target_buses: list[int] = np.flatnonzero(station.injection_switching_table[:, asset_index]).tolist() + + if len(target_buses) > 1: + raise NotImplementedError("Connecting an asset to multiple buses is not supported.") + + if len(target_buses) == 0: + net[pp_table].loc[pp_id, "in_service"] = False + injection_disconnection_diff.append(int(asset_index)) + continue + + target_bus_index = target_buses[0] + target_bus_id = station_bus_ids[target_bus_index] + previous_bus = reassign_asset_to_bus( + net=net, + asset_id=pp_id, + asset_table=pp_table, + target_bus_id=target_bus_id, + station_buses=station_buses, + ) + if previous_bus != target_bus_id: + injection_reassignment_diff.append((int(asset_index), int(target_bus_index), True)) + try: + previous_bus_index = station_bus_ids.index(previous_bus) + except ValueError: + previous_bus_index = -1 + logger.warning( + f"Asset {asset.grid_model_id} was reassigned from bus {previous_bus} which is not in the station." + ) + injection_reassignment_diff.append((int(asset_index), int(previous_bus_index), False)) + + return injection_disconnection_diff, injection_reassignment_diff + + +def apply_station_assets( + net: pp.pandapowerNet, + station: MaterializedStation, +) -> tuple[ + list[int], + list[int], + list[tuple[int, int, bool]], + list[tuple[int, int, bool]], +]: """Apply a station topology to a pandapower network. This assumes that exactly the assets in the station also exist in the pandapower network. @@ -298,10 +454,15 @@ def apply_station_assets( Returns ------- list[int] - A list of asset indices that were disconnected from the grid. + Branch indices that were disconnected from the grid. + list[int] + Injection indices that were disconnected from the grid. + list[tuple[int, int, bool]] + Branch reassignment tuples containing the branch index, the bus id to which it was assigned/unassigned and a + boolean indicating whether the branch was assigned (true) or unassigned (false). list[tuple[int, int, bool]] - A list of tuples containing the asset index, the bus id to which it was assigned/unassigned and a boolean indicating - whether the asset was assigned (true) or unassigned (false). + Injection reassignment tuples containing the injection index, the bus id to which it was assigned/unassigned and a + boolean indicating whether the injection was assigned (true) or unassigned (false). """ assert all(table_id(bus.grid_model_id) in net.bus.index for bus in station.busbars), ( "All busbars must be present in the pandapower network." @@ -311,57 +472,34 @@ def apply_station_assets( net=net, buses=[table_id(bus.grid_model_id) for bus in station.busbars], consider=("s",), respect_switches=False ) - disconnection_diff = [] - reassignment_diff = [] - for asset_index, asset_connection in enumerate(station.asset_connections): - asset = asset_connection.asset - pp_id, asset_type = parse_globally_unique_id(asset.grid_model_id) - pp_table = get_element_table(asset_type) - pp_id = int(pp_id) - # Note that this is an index into the asset switching table, not into the pandapower net.buses table - target_buses: list[int] = np.flatnonzero(station.asset_switching_table[:, asset_index]).tolist() + station_bus_ids = [table_id(bus.grid_model_id) for bus in station.busbars] - if len(target_buses) > 1: - raise NotImplementedError("Connecting an asset to multiple buses is not supported.") + branch_disconnection_diff, branch_reassignment_diff = _apply_branch_assets( + net=net, + station=station, + station_buses=station_buses, + station_bus_ids=station_bus_ids, + ) + injection_disconnection_diff, injection_reassignment_diff = _apply_injection_assets( + net=net, + station=station, + station_buses=station_buses, + station_bus_ids=station_bus_ids, + ) - if len(target_buses) == 0: - # We are supposed to disconnect the asset, we do that by setting the in_service flag to False - net[pp_table].loc[pp_id, "in_service"] = False - disconnection_diff.append(asset_index) - else: - # We shall reassign the asset to a new bus - # Note that the target_bus_index indexes into the asset switching table and the target_bus_id indexes into the - # pandapower net.buses table - target_bus_index = target_buses[0] - target_bus_id = table_id(station.busbars[target_bus_index].grid_model_id) - previous_bus = reassign_asset_to_bus( - net=net, - asset_id=pp_id, - asset_table=pp_table, - target_bus_id=target_bus_id, - station_buses=station_buses, - ) - if previous_bus != target_bus_id: - reassignment_diff.append((asset_index, target_bus_index, True)) - try: - # It could be that the asset is connected to a busbar that is not in the station but in the pandapower - # grid. In that case, we don't know where it came from. - previous_bus_index = [table_id(bus.grid_model_id) for bus in station.busbars].index(previous_bus) - except ValueError: - previous_bus_index = -1 - logger.warning( - f"Asset {asset.grid_model_id} was reassigned from bus {previous_bus} which is not in the station." - ) - reassignment_diff.append((asset_index, previous_bus_index, False)) - - return disconnection_diff, reassignment_diff + return ( + branch_disconnection_diff, + injection_disconnection_diff, + branch_reassignment_diff, + injection_reassignment_diff, + ) def apply_station_couplers( net: pp.pandapowerNet, couplers: list[BusbarCoupler], ) -> list[BusbarCoupler]: - """Apply coupler changes from an asset topology station to a pandapower network + """Apply coupler changes from an asset topology station to a pandapower network. This will find all couplers and change them to their desired state. If they have been switched, they will be returned as a coupler diff. @@ -429,8 +567,8 @@ def apply_station( """Apply an asset topology station to a pandapower network. This will force the station into the format of the asset topology, meaning missing busbars and switches will be created, - excess busbars and switches will be deleted, the asset_switching_table will be applied and the couplers will be set to - their desired state. + excess busbars and switches will be deleted, the split branch and injection switching tables will be applied and the + couplers will be set to their desired state. The asset bays and coupler bays in the station will not be recreated, i.e. if a node/breaker model is passed into this function, it will return as a bus/branch model. @@ -463,7 +601,12 @@ def apply_station( couplers=station.couplers, ) - disconnection_diff, reassignment_diff = apply_station_assets( + ( + branch_disconnection_diff, + injection_disconnection_diff, + branch_reassignment_diff, + injection_reassignment_diff, + ) = apply_station_assets( net=net, station=station, ) @@ -510,8 +653,10 @@ def apply_station( AppliedStation( station=station, coupler_diff=coupler_diff, - reassignment_diff=reassignment_diff, - disconnection_diff=disconnection_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ), ) @@ -520,7 +665,8 @@ def apply_topology(net: pp.pandapowerNet, topology: Topology) -> tuple[list[tupl """Apply an asset topology to a pandapower network. This will apply all stations in the topology to the pandapower network. It will create missing busbars and switches, - delete excess busbars and switches, apply the asset_switching_table and set the couplers to their desired state. + delete excess busbars and switches, apply the split branch and injection switching tables and set the couplers to + their desired state. If switches or busbars had to be adjusted, this is returned separately. @@ -543,11 +689,19 @@ def apply_topology(net: pp.pandapowerNet, topology: Topology) -> tuple[list[tupl apply_diffs = [(rs.station.grid_model_id, apply_diff) for apply_diff, rs in realizations] realized_stations = [rs for _, rs in realizations] - coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) + ( + coupler_diff, + branch_reassignment_diff, + injection_reassignment_diff, + branch_disconnection_diff, + injection_disconnection_diff, + ) = accumulate_diffs(realized_stations) return apply_diffs, RealizedTopology( topology=topology, coupler_diff=coupler_diff, - reassignment_diff=reassignment_diff, - disconnection_diff=disconnection_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index 83a91c3e8..85d19897c 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -23,7 +23,7 @@ import pandera as pa import pandera.typing as pat import structlog -from beartype.typing import Literal, Optional, Union, cast +from beartype.typing import Literal, Optional, Union from pypowsybl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import assert_station_in_network from toop_engine_interfaces.asset_topology import ( @@ -44,7 +44,23 @@ @pa.check_types def get_coupler_states_from_busbar_couplers(station_couplers: list[BusbarCoupler]) -> pat.DataFrame[SwitchUpdateSchema]: - """Translate the coupler states to the switch update schema format.""" + """Translate coupler states to the switch-update schema format. + + Parameters + ---------- + station_couplers : list[BusbarCoupler] + Couplers whose target open state should be expressed as switch updates. + + Returns + ------- + pat.DataFrame[SwitchUpdateSchema] + Switch-update rows containing the desired state for each coupler. + + Raises + ------ + ValueError + If a coupler is marked out of service. + """ switch_df = get_empty_dataframe_from_model(SwitchUpdateSchema) for coupler in station_couplers: if not coupler.in_service: @@ -54,46 +70,63 @@ def get_coupler_states_from_busbar_couplers(station_couplers: list[BusbarCoupler "open": coupler.open, } switch_df = switch_df.astype({"grid_model_id": str, "open": bool}) - return cast(pat.DataFrame[SwitchUpdateSchema], switch_df) + return switch_df -def get_asset_bay_grid_model_id_list(station: MaterializedStation) -> list[dict[str, str] | None]: - """Get selector switch ids for each station asset.""" - sr_switch_grid_model_id_list: list[dict[str, str] | None] = [] - for asset_connection in station.asset_connections: - asset_bay = asset_connection.asset_bay - if asset_bay is None or asset_bay.sr_switch_grid_model_id is None: - sr_switch_grid_model_id_list.append(None) - else: - sr_switch_grid_model_id_list.append(asset_bay.sr_switch_grid_model_id) - return sr_switch_grid_model_id_list +def get_busbar_lookup(station: MaterializedStation) -> dict[int, str]: + """Map switching-table row indices to station busbar ids. + Parameters + ---------- + station : MaterializedStation + Station whose busbar ordering defines the switching-table row order. -def get_busbar_lookup(station: MaterializedStation) -> dict[int, str]: - """Get the busbar lookup for the given station.""" + Returns + ------- + dict[int, str] + Mapping from switching-table row index to busbar ``grid_model_id``. + """ return {index: busbar.grid_model_id for index, busbar in enumerate(station.busbars)} -@pa.check_types -def get_asset_switch_states_from_station( +def _get_branch_switch_states_from_station( station: MaterializedStation, -) -> tuple[pat.DataFrame[SwitchUpdateSchema], pat.DataFrame[SwitchUpdateSchema]]: - """Translate asset selector and breaker states of one station to switch updates.""" + busbar_id_dict: dict[int, str], +) -> tuple[list[dict[str, str | bool]], list[dict[str, str | bool]]]: + """Translate branch selector and breaker states of one station to switch updates. + + Parameters + ---------- + station : MaterializedStation + Station whose branch-side switching state should be exported. + busbar_id_dict : dict[int, str] + Mapping from branch switching-table row index to busbar id. + + Returns + ------- + tuple[list[dict[str, str | bool]], list[dict[str, str | bool]]] + Two lists containing branch reassignment switch states and branch + disconnection switch states. + + Raises + ------ + ValueError + If a branch switching-table column connects the same branch to multiple + busbars. + """ switch_reassignment_list: list[dict[str, str | bool]] = [] switch_disconnection_list: list[dict[str, str | bool]] = [] - busbar_id_dict = get_busbar_lookup(station) - switching_table = np.asarray(station.asset_switching_table, dtype=bool) - asset_reassignment_list = get_asset_bay_grid_model_id_list(station) - assert switching_table.shape[1] == len(asset_reassignment_list), ( - "The asset switching table has a different number of columns than the asset reassignment list. " - f"Columns: {switching_table.shape[1]}, Reassignment list: {len(asset_reassignment_list)}" + + assert station.branch_switching_table.shape[1] == len(station.branch_connections), ( + "The branch switching table has a different number of columns than the branch reassignment list. " + f"Columns: {station.branch_switching_table.shape[1]}, Assets: {len(station.branch_connections)}" ) - for column in range(switching_table.shape[1]): - asset_switch_ids = asset_reassignment_list[column] + for column, asset_connection in enumerate(station.branch_connections): + asset_switch_ids = asset_connection.get_sr_switch() if asset_switch_ids is None: continue - asset_switch_states = switching_table[:, column] + asset_switch_states = station.branch_switching_table[:, column] active_busbars = int(asset_switch_states.sum()) if active_busbars == 1: assigned_busbar = int(np.nonzero(asset_switch_states)[0][0]) @@ -105,7 +138,7 @@ def get_asset_switch_states_from_station( } ) elif active_busbars == 0: - asset_bay = station.asset_connections[column].asset_bay + asset_bay = asset_connection.asset_bay assert asset_bay is not None switch_disconnection_list.append( { @@ -114,7 +147,107 @@ def get_asset_switch_states_from_station( } ) else: - raise ValueError(f"Switching table column {column} has more than one True value: {switching_table[:, column]}") + raise ValueError( + f"Switching table column {column} has more than one True value: {station.branch_switching_table[:, column]}" + ) + + return switch_reassignment_list, switch_disconnection_list + + +def _get_injection_switch_states_from_station( + station: MaterializedStation, + busbar_id_dict: dict[int, str], +) -> tuple[list[dict[str, str | bool]], list[dict[str, str | bool]]]: + """Translate injection selector and breaker states of one station to switch updates. + + Parameters + ---------- + station : MaterializedStation + Station whose injection-side switching state should be exported. + busbar_id_dict : dict[int, str] + Mapping from injection switching-table row index to busbar id. + + Returns + ------- + tuple[list[dict[str, str | bool]], list[dict[str, str | bool]]] + Two lists containing injection reassignment switch states and injection + disconnection switch states. + + Raises + ------ + ValueError + If an injection switching-table column connects the same injection to + multiple busbars. + """ + switch_reassignment_list: list[dict[str, str | bool]] = [] + switch_disconnection_list: list[dict[str, str | bool]] = [] + + assert station.injection_switching_table.shape[1] == len(station.injection_connections), ( + "The injection switching table has a different number of columns than the injection reassignment list. " + f"Columns: {station.injection_switching_table.shape[1]}, Assets: {len(station.injection_connections)}" + ) + + for column, asset_connection in enumerate(station.injection_connections): + asset_switch_ids = asset_connection.get_sr_switch() + if asset_switch_ids is None: + continue + asset_switch_states = station.injection_switching_table[:, column] + active_busbars = int(asset_switch_states.sum()) + if active_busbars == 1: + assigned_busbar = int(np.nonzero(asset_switch_states)[0][0]) + for busbar, switch_id in asset_switch_ids.items(): + switch_reassignment_list.append( + { + "grid_model_id": switch_id, + "open": busbar_id_dict[assigned_busbar] != busbar, + } + ) + elif active_busbars == 0: + asset_bay = asset_connection.asset_bay + assert asset_bay is not None + switch_disconnection_list.append( + { + "grid_model_id": asset_bay.dv_switch_grid_model_id, + "open": True, + } + ) + else: + raise ValueError( + f"Switching table column {column} has more than one True value: " + f"{station.injection_switching_table[:, column]}" + ) + + return switch_reassignment_list, switch_disconnection_list + + +@pa.check_types +def get_asset_switch_states_from_station( + station: MaterializedStation, +) -> tuple[pat.DataFrame[SwitchUpdateSchema], pat.DataFrame[SwitchUpdateSchema]]: + """Translate station asset switch states to switch-update dataframes. + + Parameters + ---------- + station : MaterializedStation + Station whose branch and injection switching tables should be exported. + + Returns + ------- + tuple[pat.DataFrame[SwitchUpdateSchema], pat.DataFrame[SwitchUpdateSchema]] + Two dataframes containing reassignment-related switch updates and + disconnection-related switch updates. + """ + busbar_id_dict = get_busbar_lookup(station) + branch_reassignment_list, branch_disconnection_list = _get_branch_switch_states_from_station( + station=station, + busbar_id_dict=busbar_id_dict, + ) + injection_reassignment_list, injection_disconnection_list = _get_injection_switch_states_from_station( + station=station, + busbar_id_dict=busbar_id_dict, + ) + switch_reassignment_list = [*branch_reassignment_list, *injection_reassignment_list] + switch_disconnection_list = [*branch_disconnection_list, *injection_disconnection_list] switch_reassignment_df = pd.DataFrame.from_records(switch_reassignment_list, columns=["grid_model_id", "open"]) switch_disconnection_df = pd.DataFrame.from_records(switch_disconnection_list, columns=["grid_model_id", "open"]) @@ -124,9 +257,7 @@ def get_asset_switch_states_from_station( switch_disconnection_df = get_empty_dataframe_from_model(SwitchUpdateSchema) switch_reassignment_df = switch_reassignment_df.astype({"grid_model_id": str, "open": bool}) switch_disconnection_df = switch_disconnection_df.astype({"grid_model_id": str, "open": bool}) - return cast(pat.DataFrame[SwitchUpdateSchema], switch_reassignment_df), cast( - pat.DataFrame[SwitchUpdateSchema], switch_disconnection_df - ) + return switch_reassignment_df, switch_disconnection_df @pa.check_types @@ -134,7 +265,26 @@ def get_diff_switch_states( network: Network, switch_df: pat.DataFrame[SwitchUpdateSchema], ) -> pat.DataFrame[SwitchUpdateSchema]: - """Filter switch updates down to the ones that differ from the current network state.""" + """Filter switch updates down to the ones differing from the network state. + + Parameters + ---------- + network : Network + Powsybl network providing the current switch states. + switch_df : pat.DataFrame[SwitchUpdateSchema] + Candidate switch updates. + + Returns + ------- + pat.DataFrame[SwitchUpdateSchema] + Only the switch updates whose target ``open`` value differs from the + current network state. + + Raises + ------ + ValueError + If a switch id from ``switch_df`` is missing in the network. + """ diff_switch_df = switch_df.merge( network.get_switches(attributes=["open"]), left_on="grid_model_id", @@ -150,12 +300,27 @@ def get_diff_switch_states( diff_switch_df = diff_switch_df[diff_switch_df["open"] != diff_switch_df["open_network"]] diff_switch_df = diff_switch_df[["grid_model_id", "open"]] diff_switch_df = diff_switch_df.astype({"grid_model_id": str, "open": bool}) - return cast(pat.DataFrame[SwitchUpdateSchema], diff_switch_df) + return diff_switch_df @pa.check_types def get_changing_switches_from_topology(network: Network, target_topology: Topology) -> pat.DataFrame[SwitchUpdateSchema]: - """Get the switch updates needed to realize a target topology on a node-breaker network.""" + """Get switch updates needed to realize a topology on a node-breaker network. + + Parameters + ---------- + network : Network + Powsybl network whose current switch states act as the reference. + target_topology : Topology + Topology whose couplers and split branch/injection station tables define + the target switch states. + + Returns + ------- + pat.DataFrame[SwitchUpdateSchema] + Switch updates required to transform the current network state into the + target node-breaker topology. + """ switch_update_df = get_empty_dataframe_from_model(SwitchUpdateSchema) for station in target_topology.materialize_stations(): coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) @@ -174,12 +339,12 @@ def get_changing_switches_from_topology(network: Network, target_topology: Topol switch_update_df = switch_update_df.astype({"grid_model_id": str, "open": bool}) return get_diff_switch_states( network=network, - switch_df=cast(pat.DataFrame[SwitchUpdateSchema], switch_update_df), + switch_df=switch_update_df, ) -def find_asset(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) -> tuple[bool, bool, bool, Optional[str]]: - """Find out whether a given element is a branch, if its connected and if branch, which direction it has. +def find_branch(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) -> tuple[bool, bool, Optional[str]]: + """Find a branch connection in one station and resolve its local orientation. It will try to match the branch with two methods. First, the voltage level id is compared. However, there are branches which have the same voltage level id on both sides, in which case @@ -199,21 +364,18 @@ def find_asset(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) - Returns ------- - bool - Whether the element is a branch bool Whether the element is connected. bool Whether side 1 is on the busbar that was passed in. This is always false if the element - is not a branch or if it is not connected. + is not connected. Optional[str] - The bus_breaker_id that the element is connected to. If the element is a branch it refers - to the side that was found. Might be None if the element is not connected. + The bus_breaker_id on the matched branch side. Might be None if the element is not connected. Raises ------ ValueError - If the element is a branch and connected, but not found in the station + If the branch is not found in the station. """ branches_df = net.get_branches( attributes=[ @@ -228,12 +390,7 @@ def find_asset(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) - ] ) if elem_id not in branches_df.index: - injections_df = net.get_injections(attributes=["connected", "voltage_level_id", "bus_id", "bus_breaker_bus_id"]) - - if elem_id not in injections_df.index or injections_df.loc[elem_id]["voltage_level_id"] != voltage_level_id: - raise ValueError(f"Element {elem_id} not found in the station.") - - return False, bool(injections_df.loc[elem_id]["connected"]), False, injections_df.loc[elem_id]["bus_breaker_bus_id"] + raise ValueError(f"Branch {elem_id} not found in the station, assumed to be in {voltage_level_id}") branch_entry = branches_df.loc[elem_id] @@ -241,7 +398,6 @@ def find_asset(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) - if (not same_vl and branch_entry["voltage_level1_id"] == voltage_level_id) or branch_entry["bus1_id"] == bus_id: return ( - True, bool(branch_entry["connected1"] and branch_entry["connected2"]), True, branch_entry["bus_breaker_bus1_id"], @@ -249,7 +405,6 @@ def find_asset(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) - if (not same_vl and branch_entry["voltage_level2_id"] == voltage_level_id) or branch_entry["bus2_id"] == bus_id: return ( - True, bool(branch_entry["connected1"] and branch_entry["connected2"]), False, branch_entry["bus_breaker_bus2_id"], @@ -258,19 +413,50 @@ def find_asset(net: Network, elem_id: str, voltage_level_id: str, bus_id: str) - raise ValueError(f"Branch {elem_id} not found in the station, assumed to be in {voltage_level_id}") +def find_injection(net: Network, elem_id: str, voltage_level_id: str) -> tuple[bool, Optional[str]]: + """Find an injection connection in one station. + + Parameters + ---------- + net : Network + Powsybl network to query. + elem_id : str + Injection id to locate. + voltage_level_id : str + Voltage level id expected for the injection. + + Returns + ------- + tuple[bool, Optional[str]] + Whether the injection is connected and the bus-breaker bus id on which + it is currently placed. + + Raises + ------ + ValueError + If the injection is not present in the expected station. + """ + injections_df = net.get_injections(attributes=["connected", "voltage_level_id", "bus_id", "bus_breaker_bus_id"]) + + if elem_id not in injections_df.index or injections_df.loc[elem_id]["voltage_level_id"] != voltage_level_id: + raise ValueError(f"Element {elem_id} not found in the station.") + + return bool(injections_df.loc[elem_id]["connected"]), injections_df.loc[elem_id]["bus_breaker_bus_id"] + + def move_branch(net: Network, elem_id: str, bus_breaker_id: str, from_end: bool) -> None: - """Move a branch to a new busbar in the network + """Move a branch to a new busbar in the network. Parameters ---------- net : Network - The powsybl network to disconnect the injection in + The powsybl network to update in place. elem_id : str - The id of the injection to disconnect + The id of the branch to move. bus_breaker_id : str - The id of the busbar to move the injection to + The id of the target bus-breaker bus. from_end : bool - Whether the branch is connected "from" side the busbar or "to" side the busbar. + Whether the matched station side corresponds to branch end 1. """ if from_end: net.update_branches(id=elem_id, bus_breaker_bus1_id=bus_breaker_id, connected1=True) @@ -287,9 +473,9 @@ def disconnect_branch( Parameters ---------- net : Network - The powsybl network to disconnect the injection in + The powsybl network to update in place. elem_id : str - The id of the injection to disconnect + The id of the branch to disconnect. """ net.update_branches(id=elem_id, connected1=False, connected2=False) @@ -304,11 +490,11 @@ def move_injection( Parameters ---------- net : Network - The powsybl network to disconnect the injection in + The powsybl network to update in place. elem_id : str - The id of the injection to disconnect + The id of the injection to move. bus_breaker_id : str - The id of the busbar to move the injection to + The id of the target bus-breaker bus. """ net.update_injections(id=elem_id, bus_breaker_bus_id=bus_breaker_id, connected=True) @@ -317,113 +503,230 @@ def disconnect_injection( net: Network, elem_id: str, ) -> None: - """Disconnects an injection in the network + """Disconnect an injection in the network. Parameters ---------- net : Network - The powsybl network to disconnect the injection in + The powsybl network to update in place. elem_id : str - The id of the injection to disconnect + The id of the injection to disconnect. """ net.update_injections(id=elem_id, connected=False) -def apply_single_asset_bus_branch( +def _get_target_bus_index( + station: MaterializedStation, + switching_column: np.ndarray, + bus_breaker_id: Optional[str], +) -> int: + """Resolve the target busbar index for one switching-table column. + + Parameters + ---------- + station : MaterializedStation + Station whose busbar order defines valid target indices. + switching_column : np.ndarray + One branch or injection switching-table column. + bus_breaker_id : Optional[str] + Current bus-breaker id of the asset, if known. + + Returns + ------- + int + Index of the target busbar. If the current bus-breaker id matches one of + the active busbars, that index is preferred; otherwise the first active + entry is used. + """ + target_bus_indices = [ + index + for index, (connected, busbar) in enumerate(zip(switching_column.tolist(), station.busbars, strict=True)) + if connected and busbar.grid_model_id == bus_breaker_id + ] + if len(target_bus_indices) == 1: + return target_bus_indices[0] + return int(np.argmax(switching_column)) + + +def _apply_single_branch_bus_branch( net: Network, station: MaterializedStation, asset_index: int, ) -> tuple[Literal["disconnected", "reassigned", "nothing"], list[tuple[int, int, bool]]]: - """Reassign or disconnect a single asset in a bus/branch topology + """Reassign or disconnect a single branch connection in a bus/branch topology. + + Parameters + ---------- + net : Network + The powsybl network to apply the topology to. It is assumed that the station is fully present in the + network. + station : MaterializedStation + The asset topology station in which the branch at position asset_index will be applied to the powsybl network. + asset_index : int + The index of the branch connection inside station.branch_connections. + + Returns + ------- + Literal["disconnected", "reassigned", "nothing"] + A string indicating whether the branch was disconnected, reassigned or left as is. + list[tuple[int, int, bool]] + A list of reassignment diffs for this branch. + """ + vl_id = net.get_buses(attributes=["voltage_level_id"]).loc[station.grid_model_id]["voltage_level_id"] - If the asset has no connections in the switching table, it is set to be disconnected. If it isn't already - disconnected, it will be disconnected in the net and 'disconnected' will be returned. + asset = station.branch_connections[asset_index].asset + switching_column = station.branch_switching_table[:, asset_index] + + is_connected, from_side, bus_breaker_id = find_branch( + net=net, elem_id=asset.grid_model_id, voltage_level_id=vl_id, bus_id=station.grid_model_id + ) + + if not np.any(switching_column): + if is_connected: + disconnect_branch(net=net, elem_id=asset.grid_model_id) + return "disconnected", [] + return "nothing", [] + + target_bus_index = _get_target_bus_index(station, switching_column, bus_breaker_id) + target_bus_breaker_id = station.busbars[target_bus_index].grid_model_id + if target_bus_breaker_id == bus_breaker_id: + return "nothing", [] + + move_branch( + net=net, + elem_id=asset.grid_model_id, + bus_breaker_id=target_bus_breaker_id, + from_end=from_side, + ) + reassignments = [(asset_index, target_bus_index, True)] + + old_indices = [index for index, busbar in enumerate(station.busbars) if busbar.grid_model_id == bus_breaker_id] + if len(old_indices) == 1: + reassignments.append((asset_index, old_indices[0], False)) + + return "reassigned", reassignments - If the asset has one or more switching table entries, but none of them matches the current bus_breaker assignment, - the asset needs to be reassigned. In this case it will be reassigned to the first busbar that is True in the switching - table and 'reassigned' will be returned. - If neither a disconnection or a reassignment is needed, 'nothing' will be returned and the resulting powsybl network - will be the same as before. +def apply_single_branch_bus_branch( + net: Network, + station: MaterializedStation, +) -> tuple[list[int], list[tuple[int, int, bool]]]: + """Apply all branch connection updates for one station in bus/branch topology. + + Parameters + ---------- + net : Network + Powsybl network to modify in place. + station : MaterializedStation + Station whose branch-side topology should be realized. + + Returns + ------- + tuple[list[int], list[tuple[int, int, bool]]] + Disconnected branch indices and branch reassignment diffs. + """ + branch_disconnection_diff: list[int] = [] + branch_reassignment_diff: list[tuple[int, int, bool]] = [] + + for asset_index in range(len(station.branch_connections)): + result, new_reassignments = _apply_single_branch_bus_branch(net, station, asset_index) + if result == "disconnected": + branch_disconnection_diff.append(asset_index) + elif result == "reassigned": + branch_reassignment_diff.extend( + [(int(asset_index), int(busbar_index), bool(connected)) for _, busbar_index, connected in new_reassignments] + ) + + return branch_disconnection_diff, branch_reassignment_diff + + +def _apply_single_injection_bus_branch( + net: Network, + station: MaterializedStation, + asset_index: int, +) -> tuple[Literal["disconnected", "reassigned", "nothing"], list[tuple[int, int, bool]]]: + """Reassign or disconnect a single injection connection in a bus/branch topology. Parameters ---------- net : Network The powsybl network to apply the topology to. It is assumed that the station is fully present in the network. - station : Station - The asset topology station in which the asset at position asset_index will be applied to the powsybl network. + station : MaterializedStation + The asset topology station in which the injection at position asset_index will be applied to the powsybl network. asset_index : int - The index of the asset connection in the station switching table. + The index of the injection connection inside station.injection_connections. Returns ------- Literal["disconnected", "reassigned", "nothing"] - A string indicating whether the asset was disconnected, reassigned or left as is. + A string indicating whether the injection was disconnected, reassigned or left as is. list[tuple[int, int, bool]] - A list of reassignments that have been made. Each tuple contains the asset index that was - affected (not the asset grid_model_id but the index into the asset_switching_table), the busbar - index (again the index into the switching table) and whether the asset was connected (True) or - disconnected (False) to that busbar. Note that this list will have either 0, 1 or 2 entries and - the first entry of the tuple, the asset index, will be the same as the asset_index passed in. - If no reassignments happened, this will be the empty list. - - Raises - ------ - ValueError - If the asset is not in the station or if the asset is not in the network + A list of reassignment diffs for this injection. """ vl_id = net.get_buses(attributes=["voltage_level_id"]).loc[station.grid_model_id]["voltage_level_id"] - asset = station.asset_connections[asset_index].asset - is_branch, is_connected, from_side, bus_breaker_id = find_asset( - net=net, elem_id=asset.grid_model_id, voltage_level_id=vl_id, bus_id=station.grid_model_id + asset = station.injection_connections[asset_index].asset + switching_column = station.injection_switching_table[:, asset_index] + + is_connected, bus_breaker_id = find_injection(net=net, elem_id=asset.grid_model_id, voltage_level_id=vl_id) + + if not np.any(switching_column): + if is_connected: + disconnect_injection(net=net, elem_id=asset.grid_model_id) + return "disconnected", [] + return "nothing", [] + + target_bus_index = _get_target_bus_index(station, switching_column, bus_breaker_id) + target_bus_breaker_id = station.busbars[target_bus_index].grid_model_id + if target_bus_breaker_id == bus_breaker_id: + return "nothing", [] + + move_injection( + net=net, + elem_id=asset.grid_model_id, + bus_breaker_id=target_bus_breaker_id, ) + reassignments = [(asset_index, target_bus_index, True)] - switching_column = station.asset_switching_table[:, asset_index] + old_indices = [index for index, busbar in enumerate(station.busbars) if busbar.grid_model_id == bus_breaker_id] + if len(old_indices) == 1: + reassignments.append((asset_index, old_indices[0], False)) - target_disconnected = not np.any(switching_column) + return "reassigned", reassignments - # If the bus_breaker_id of the asset is currently set in the switching table, use - # that as the target bus_breaker_id. Otherwise, just use the first True entry. - target_bus_indices = [ - index - for index, (connected, busbar) in enumerate(zip(switching_column.tolist(), station.busbars, strict=True)) - if connected and busbar.grid_model_id == bus_breaker_id - ] - if len(target_bus_indices) == 1: - target_bus_index = target_bus_indices[0] - else: - target_bus_index = int(np.argmax(switching_column)) - if target_disconnected and is_connected: - if is_branch: - disconnect_branch(net=net, elem_id=asset.grid_model_id) - else: - disconnect_injection(net=net, elem_id=asset.grid_model_id) - return "disconnected", [] - if station.busbars[target_bus_index].grid_model_id != bus_breaker_id: - if is_branch: - move_branch( - net=net, - elem_id=asset.grid_model_id, - bus_breaker_id=station.busbars[target_bus_index].grid_model_id, - from_end=from_side, - ) - else: - move_injection( - net=net, - elem_id=asset.grid_model_id, - bus_breaker_id=station.busbars[target_bus_index].grid_model_id, - ) - reassignments = [(asset_index, target_bus_index, True)] +def apply_single_injection_bus_branch( + net: Network, + station: MaterializedStation, +) -> tuple[list[int], list[tuple[int, int, bool]]]: + """Apply all injection connection updates for one station in bus/branch topology. - old_indices = [index for index, busbar in enumerate(station.busbars) if busbar.grid_model_id == bus_breaker_id] - if len(old_indices) == 1: - reassignments.append((asset_index, old_indices[0], False)) + Parameters + ---------- + net : Network + Powsybl network to modify in place. + station : MaterializedStation + Station whose injection-side topology should be realized. - return "reassigned", reassignments - return "nothing", [] + Returns + ------- + tuple[list[int], list[tuple[int, int, bool]]] + Disconnected injection indices and injection reassignment diffs. + """ + injection_disconnection_diff: list[int] = [] + injection_reassignment_diff: list[tuple[int, int, bool]] = [] + + for asset_index in range(len(station.injection_connections)): + result, new_reassignments = _apply_single_injection_bus_branch(net, station, asset_index) + if result == "disconnected": + injection_disconnection_diff.append(asset_index) + elif result == "reassigned": + injection_reassignment_diff.extend( + [(int(asset_index), int(busbar_index), bool(connected)) for _, busbar_index, connected in new_reassignments] + ) + + return injection_disconnection_diff, injection_reassignment_diff def set_coupler( @@ -431,21 +734,26 @@ def set_coupler( coupler_id: str, target_state: bool, ) -> bool: - """Set the state of a coupler in the network + """Set the state of a coupler in the network. Parameters ---------- net : Network - The powsybl network to switch the coupler in, will be modified in-place + The powsybl network to switch the coupler in, modified in place. coupler_id : str - The id of the coupler to switch, should be in net.get_switches + The id of the coupler to switch, expected in ``net.get_switches()``. target_state : bool - The target state of the coupler, True for open, False for closed + Target switch state, ``True`` for open and ``False`` for closed. Returns ------- bool - True if the coupler was switched, False if it was already in the target state + ``True`` if the coupler was switched, ``False`` if it already had the target state. + + Raises + ------ + ValueError + If the coupler is missing in the network. """ switches_df = net.get_switches(attributes=["open"]) if coupler_id not in switches_df.index: @@ -457,7 +765,7 @@ def set_coupler( def apply_station_bus_branch(net: Network, station: MaterializedStation) -> AppliedStation: - """Apply a station topology to a powsybl model in bus/branch format + """Apply a station topology to a powsybl model in bus/branch format. This will assume that the substations are in bus/branch format and that the busbars in the station are the same as in the asset topology, just that the assets are not yet on their spot and the busbar couplers are not yet correctly switched @@ -471,8 +779,9 @@ def apply_station_bus_branch(net: Network, station: MaterializedStation) -> Appl asset topology. If there are more buses, these additional buses will be ignored, if there are fewer buses, an exception is raised. Will be modified in-place. station : MaterializedStation - The asset topology station. The switching state of the assets and busbar couplers shall be applied to the matched - station in the powsybl grid + The asset topology station. The split branch and injection switching + tables, plus the coupler states, are applied to the matched station in + the powsybl grid. Returns ------- @@ -487,16 +796,9 @@ def apply_station_bus_branch(net: Network, station: MaterializedStation) -> Appl """ assert_station_in_network(net, station, couplers_strict=False, assets_strict=False, busbars_strict=False) - disconnection_diff = [] - reassignment_diff = [] coupler_diff = [] - - for asset_index in range(len(station.asset_connections)): - result, new_reassignments = apply_single_asset_bus_branch(net, station, asset_index) - if result == "disconnected": - disconnection_diff.append(asset_index) - elif result == "reassigned": - reassignment_diff.extend(new_reassignments) + branch_disconnection_diff, branch_reassignment_diff = apply_single_branch_bus_branch(net, station) + injection_disconnection_diff, injection_reassignment_diff = apply_single_injection_bus_branch(net, station) for coupler in station.couplers: if set_coupler(net, coupler.grid_model_id, coupler.open): @@ -504,14 +806,16 @@ def apply_station_bus_branch(net: Network, station: MaterializedStation) -> Appl return AppliedStation( station=station, - disconnection_diff=disconnection_diff, - reassignment_diff=reassignment_diff, coupler_diff=coupler_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ) def apply_topology_bus_branch(net: Network, topology: Topology) -> RealizedTopology: - """Apply an asset topology to a network and return the diff + """Apply an asset topology to a network and return the diff. This takes an asset topology and applies it to the network. It will return the diff that it had to do to reach the asset topology in the form of a RealizedTopology @@ -532,37 +836,44 @@ def apply_topology_bus_branch(net: Network, topology: Topology) -> RealizedTopol Returns ------- RealizedTopology - The realized topology object which contains the input topology plus a diff of switched couplers, reassignments and - disconnections. + The realized topology object containing the input topology plus the + coupler, reassignment, and disconnection diffs. """ realized_stations = [apply_station_bus_branch(net, station) for station in topology.materialize_stations()] - coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) + ( + coupler_diff, + branch_reassignment_diff, + injection_reassignment_diff, + branch_disconnection_diff, + injection_disconnection_diff, + ) = accumulate_diffs(realized_stations) return RealizedTopology( topology=topology, coupler_diff=coupler_diff, - reassignment_diff=reassignment_diff, - disconnection_diff=disconnection_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ) @pa.check_types def apply_node_breaker_topology(net: Network, target_topology: Topology) -> pa.typing.DataFrame[SwitchUpdateSchema]: - """Apply a node-breaker Topology to a powsybl network + """Apply a node-breaker topology to a powsybl network. Parameters ---------- net : Network The powsybl network to modify, will be modified in place. target_topology : Topology - The target topology to apply to the network, i.e. how you want the network to look like + The target topology to apply to the network. Returns ------- - switch_update_df: SwitchUpdateSchema - The dataframe of the switches that were updated. This is the same as the one returned by get_diff_switch_states - but with the index set to the switch id. + pa.typing.DataFrame[SwitchUpdateSchema] + The dataframe of switches that were updated. """ switch_update_df = get_changing_switches_from_topology(network=net, target_topology=target_topology) switch_update_df.rename(columns={"grid_model_id": "id"}, inplace=True) @@ -576,12 +887,12 @@ def apply_node_breaker_topology(net: Network, target_topology: Topology) -> pa.t def is_node_breaker_grid(net: Network, relevant_station: Optional[str] = None) -> bool: - """Check if the network is in node-breaker format + """Check if the network is in node-breaker format. Parameters ---------- net : Network - The powsybl network to check + The powsybl network to check. relevant_station : Optional[str] The name of any relevant station to check. It is possible that a grid has a mix of node-breaker and bus/branch but the relevant stations should be all uniform, in one of the two formats. @@ -604,7 +915,7 @@ def is_node_breaker_grid(net: Network, relevant_station: Optional[str] = None) - def apply_station( net: Network, topology: Topology, raw_station: RawStation ) -> Union[pa.typing.DataFrame[SwitchUpdateSchema], AppliedStation]: - """Apply a station topology to a powsybl model + """Apply a station topology to a powsybl model. This will apply the station topology to the network. If the network is in bus/branch format, it will return the realized station. If the network is in node/breaker format, it will return the switch update dataframe. @@ -628,8 +939,19 @@ def apply_station( Union[pa.typing.DataFrame[SwitchUpdateSchema], AppliedStation] The realized station object which contains the input station plus a diff of switched couplers, reassignments and disconnections or a dataframe of switches that were updated. + + Raises + ------ + ValueError + If the station cannot be matched into the expected powsybl representation. """ - station_topology = copy_topology_with_updates(topology, [raw_station], topology.assets, topology.asset_bays) + station_topology = copy_topology_with_updates( + topology, + [raw_station], + topology.asset_bays, + branch_assets=topology.branch_assets, + injection_assets=topology.injection_assets, + ) if is_node_breaker_grid(net=net, relevant_station=raw_station.grid_model_id): return apply_node_breaker_topology( diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py index 30db75ac0..c8cb29f8e 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py @@ -32,7 +32,11 @@ from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import ( parse_globally_unique_id, ) -from toop_engine_interfaces.asset_topology import AppliedStation, RealizedTopology, topology_from_materialized_stations +from toop_engine_interfaces.asset_topology import ( + AppliedStation, + RealizedTopology, + topology_from_materialized_stations, +) from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs, electrical_components from toop_engine_interfaces.loadflow_result_helpers_polars import extract_solver_matrices_polars from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars @@ -84,15 +88,23 @@ def apply_topology( _diff, realized_station = apply_station(net, action_set.local_actions[action]) realized_stations.append(realized_station) - coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) + ( + coupler_diff, + branch_reassignment_diff, + injection_reassignment_diff, + branch_disconnection_diff, + injection_disconnection_diff, + ) = accumulate_diffs(realized_stations) realized_topology = RealizedTopology( topology=topology_from_materialized_stations( action_set.starting_topology, [s.station for s in realized_stations], ), coupler_diff=coupler_diff, - reassignment_diff=reassignment_diff, - disconnection_diff=disconnection_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ) return net, realized_topology @@ -193,50 +205,48 @@ def compute_cross_coupler_flows( assert len(components) == 2, "The split station must have exactly two sides" busbars_a = list(components[0]) - for index, asset_connection in enumerate(station.asset_connections): - asset = asset_connection.asset - asset_id, asset_type = parse_globally_unique_id(asset.grid_model_id) - asset_id = int(asset_id) - - if station.asset_switching_table[busbars_a, index].any(): - # The asset is on busbar A, include it - if asset.is_branch() is True: - branch_end = asset_connection.terminal - if branch_end is None: - raise ValueError("Branch end must be set in asset topo") - from_end = branch_end in ("from", "hv") - p_on_a += get_pandapower_branch_loadflow_results_sequence( - net, types=[asset_type], ids=[asset_id], measurement="active", from_end=from_end, adjust_signs=False - ).item() - - q_on_a += np.nan_to_num( - get_pandapower_branch_loadflow_results_sequence( - net, - types=[asset_type], - ids=[asset_id], - measurement="reactive", - from_end=from_end, - adjust_signs=False, - ) - ).item() - elif asset.is_branch() is False: - p_on_a += get_pandapower_loadflow_results_injection( + for index, asset_connection in enumerate(station.branch_connections): + if station.branch_switching_table[busbars_a, index].any(): + asset_id, asset_type = parse_globally_unique_id(asset_connection.asset.grid_model_id) + asset_id = int(asset_id) + branch_end = asset_connection.terminal + if branch_end is None: + raise ValueError("Branch end must be set in asset topo") + from_end = branch_end in ("from", "hv") + p_on_a += get_pandapower_branch_loadflow_results_sequence( + net, types=[asset_type], ids=[asset_id], measurement="active", from_end=from_end, adjust_signs=False + ).item() + + q_on_a += np.nan_to_num( + get_pandapower_branch_loadflow_results_sequence( + net, + types=[asset_type], + ids=[asset_id], + measurement="reactive", + from_end=from_end, + adjust_signs=False, + ) + ).item() + + for index, asset_connection in enumerate(station.injection_connections): + if station.injection_switching_table[busbars_a, index].any(): + asset_id, asset_type = parse_globally_unique_id(asset_connection.asset.grid_model_id) + asset_id = int(asset_id) + p_on_a += get_pandapower_loadflow_results_injection( + net, + types=[asset_type], + ids=[asset_id], + reactive=False, + ).item() + + q_on_a += np.nan_to_num( + get_pandapower_loadflow_results_injection( net, types=[asset_type], ids=[asset_id], - reactive=False, - ).item() - - q_on_a += np.nan_to_num( - get_pandapower_loadflow_results_injection( - net, - types=[asset_type], - ids=[asset_id], - reactive=True, - ) - ).item() - else: - raise ValueError("Asset type must be set in asset topo to determine branch/injection") + reactive=True, + ) + ).item() cross_coupler_flows_p.append(p_on_a) cross_coupler_flows_q.append(q_on_a) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index 10ef860c2..cc11cb8b0 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -136,11 +136,11 @@ def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> A return None stations = [action_set.local_actions[action] for action in actions] - changed_stations_topo = topology_from_materialized_stations(action_set.starting_topology, stations) - if is_node_breaker_grid(net, stations[0].grid_model_id): + changed_stations_topo = topology_from_materialized_stations(action_set.starting_topology, stations) additional_info = apply_node_breaker_topology(net, changed_stations_topo) else: + changed_stations_topo = topology_from_materialized_stations(action_set.simplified_starting_topology, stations) additional_info = apply_topology_bus_branch(net, changed_stations_topo) return additional_info @@ -266,25 +266,24 @@ def compute_cross_coupler_flows( p_sum = 0.0 q_sum = 0.0 - for index, asset_connection in enumerate(station.asset_connections): - asset = asset_connection.asset - if station.asset_switching_table[busbars_a, index].any(): - # The asset is on busbar A, include it - if asset.is_branch() is True: - branch_end = asset_connection.terminal - if branch_end is None: - raise ValueError("Branch end is None") - from_end = branch_end in ("from", "hv") - p, q = extract_single_branch_loadflow_result(branch_res, asset.grid_model_id, from_end) - p_sum += p - q_sum += q - elif asset.is_branch() is False: - p, q = extract_single_injection_loadflow_result( - injection_res, - asset.grid_model_id, - ) - p_sum += p - q_sum += q + for index, asset_connection in enumerate(station.branch_connections): + if station.branch_switching_table[busbars_a, index].any(): + branch_end = asset_connection.terminal + if branch_end is None: + raise ValueError("Branch end is None") + from_end = branch_end in ("from", "hv") + p, q = extract_single_branch_loadflow_result(branch_res, asset_connection.asset.grid_model_id, from_end) + p_sum += p + q_sum += q + + for index, asset_connection in enumerate(station.injection_connections): + if station.injection_switching_table[busbars_a, index].any(): + p, q = extract_single_injection_loadflow_result( + injection_res, + asset_connection.asset.grid_model_id, + ) + p_sum += p + q_sum += q active_power.append(p_sum) reactive_power.append(q_sum) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py index 064b8def2..044ff3489 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py @@ -427,16 +427,20 @@ def realise_ba_to_physical_topo_per_station_jax( local_branch_action_set = local_branch_action_set[1:] n_branches = local_branch_action_set.shape[1] - # Currently we ignore injection assets which are at the end of the separation set due to the sorting within the - # simplification process for the station. separation_set = separation_set[:, :, :n_branches] - asset_switching_table = station.asset_switching_table[:, :n_branches] - asset_connectivity = ( - station.asset_connectivity[:, :n_branches] - if station.asset_connectivity is not None - else np.ones_like(asset_switching_table, dtype=bool) - ) + branch_switching_table = np.asarray(station.branch_switching_table, dtype=bool) + branch_connectivity = ( + station.branch_connectivity + if station.branch_connectivity is not None + else np.ones_like(station.branch_switching_table, dtype=bool) + ) + assert branch_switching_table.shape[1] == local_branch_action_set.shape[1], ( + "The number of branches in the station must match the number of branches in the local action set." + ) + assert branch_connectivity.shape[1] == local_branch_action_set.shape[1], ( + "The number of branches in the station must match the number of branches in the local action set." + ) # Map over the local action set and compute a switching table for each action with jax.default_device(jax.devices("cpu")[0]): ( @@ -452,8 +456,8 @@ def realise_ba_to_physical_topo_per_station_jax( separation_set=jnp.array(separation_set, dtype=bool), coupler_states=jnp.array(coupler_states, dtype=bool), busbar_mapping=jnp.array(busbar_b_array, dtype=bool), - current_switching_table=jnp.array(asset_switching_table, dtype=bool), - asset_connectivity=jnp.array(asset_connectivity, dtype=bool), + current_switching_table=jnp.array(branch_switching_table, dtype=bool), + asset_connectivity=jnp.array(branch_connectivity, dtype=bool), batch_size=batch_size, choice_heuristic=choice_heuristic, ) @@ -481,9 +485,7 @@ def realise_ba_to_physical_topo_per_station_jax( realised_stations = [ station.model_copy( update={ - "asset_switching_table": np.concatenate( - [action_switching, station.asset_switching_table[:, n_branches:]], axis=1 - ), + "branch_switching_table": action_switching, "couplers": [ coupler.model_copy(update={"open": bool(open)}) for coupler, open in zip(station.couplers, action_coupler_states, strict=True) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py index c3e8caadd..59fd3af2b 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py @@ -40,7 +40,6 @@ from toop_engine_dc_solver.preprocess.helpers.switching_distance import min_hamming_distance_matrix from toop_engine_dc_solver.preprocess.network_data import NetworkData, get_relevant_stations from toop_engine_interfaces.asset_topology import RawStation -from toop_engine_interfaces.asset_topology_helpers import get_connected_assets from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits logger = structlog.get_logger(__name__) @@ -675,8 +674,11 @@ def determine_injection_topology_sub( bba_connected_injection_ids = [ asset.grid_model_id for bb_index in busbar_a_mapping - for asset in get_connected_assets(station, bb_index, network_data.simplified_asset_topology.assets) - if not asset.is_branch() + for asset in station.get_connected_assets( + bb_index, + topology_assets=network_data.simplified_asset_topology.injection_assets, + asset_scope="injection", + ) ] bba_connected_injection_idxs = [ np.argmax(local_injection_idxs == network_data.injection_ids.index(injection_id)) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/helpers/ptdf.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/helpers/ptdf.py index 9fd609696..6b67c08d9 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/helpers/ptdf.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/helpers/ptdf.py @@ -19,6 +19,7 @@ def compute_ptdf( to_node: Int[np.ndarray, " n_branch"], susceptances: Float[np.ndarray, " n_branch"], slack_bus: int, + branch_flow_susceptances: Optional[Float[np.ndarray, " n_branch"]] = None, ) -> Float[np.ndarray, " n_branch n_node"]: """Calculate the PTDF (Power Transfer Distiribution Factors). @@ -37,9 +38,12 @@ def compute_ptdf( The to-nodes vector. Changes if the topology changes, e.g. the to-bus of a branch can be set to the second bus of a substation. susceptances : Float[np.ndarray, " n_branch"] - Vector with the susceptances for the branches. + Vector with the susceptances used in the nodal angle solve. slack_bus: int The slack bus of the grid. Cannot be distributed for the bsdf formulation to work + branch_flow_susceptances : Optional[Float[np.ndarray, " n_branch"]] + Optional branch coefficients used when mapping bus angles to branch flows. If not given, + the nodal susceptances are used for both the nodal solve and the branch-flow rows. number_of_busses: int How many busbars are in the system-> How many nodes in the ptdf @@ -50,6 +54,8 @@ def compute_ptdf( """ number_of_busses = int(np.max(np.concatenate([from_node, to_node]))) + 1 number_of_branches = int(from_node.shape[0]) + if branch_flow_susceptances is None: + branch_flow_susceptances = susceptances # use bus 1 for voltage angle reference noref = np.arange(1, number_of_busses) @@ -58,7 +64,12 @@ def compute_ptdf( ptdf = np.zeros((number_of_branches, number_of_busses)) node_node_susceptance, branch_node_susceptance, *_ = get_susceptance_matrices( - from_node, to_node, susceptances, number_of_branches, number_of_busses + from_node, + to_node, + susceptances, + number_of_branches, + number_of_busses, + branch_flow_susceptances=branch_flow_susceptances, ) ptdf[:, noslack] = spsolve( node_node_susceptance[np.ix_(noslack, noref)].T, @@ -77,6 +88,7 @@ def get_susceptance_matrices( susceptances: Float[np.ndarray, " n_branch"], number_of_branches: int, number_of_busses: int, + branch_flow_susceptances: Optional[Float[np.ndarray, " n_branch"]] = None, ) -> tuple[spmatrix, spmatrix]: """Build the B matrices necessary for DC power flow and PTDF calculation. @@ -96,11 +108,14 @@ def get_susceptance_matrices( The to-nodes vector. Changes if the topology changes, e.g. the to-bus of a branch can be set to the second bus of a substation. susceptances : Float[np.ndarray, " n_branch"] - Vector with the susceptances for the branches. + Vector with the susceptances used in the nodal angle solve. number_of_branches: int How many branches are in the system number_of_busses: int How many busbars are in the system + branch_flow_susceptances : Optional[Float[np.ndarray, " n_branch"]] + Optional branch coefficients used for the branch-to-node flow matrix. If not given, + the nodal susceptances are used. Returns ------- @@ -109,14 +124,19 @@ def get_susceptance_matrices( branch_node_susceptance: Float[np.ndarray, " n_branch n_node"] BranchxNode Matrix allowing for the calculation of the power flow """ + if branch_flow_susceptances is None: + branch_flow_susceptances = susceptances + # build connection matrix Cft = Cf - Ct for line and from - to busses connectivity_matrix = get_connectivity_matrix(from_node, to_node, number_of_branches, number_of_busses) - # Build branch-to-node susceptance matrix - branch_node_susceptance = csc_matrix(connectivity_matrix.multiply(susceptances[:, np.newaxis])) + nodal_branch_node_susceptance = csc_matrix(connectivity_matrix.multiply(susceptances[:, np.newaxis])) + + # Build branch-to-node susceptance matrix used to map bus angles to branch flows. + branch_node_susceptance = csc_matrix(connectivity_matrix.multiply(branch_flow_susceptances[:, np.newaxis])) # build node_node_susceptance - node_node_susceptance = connectivity_matrix.T * branch_node_susceptance + node_node_susceptance = connectivity_matrix.T * nodal_branch_node_susceptance return node_node_susceptance, branch_node_susceptance 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 4f2c3f259..3e28c849c 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 @@ -738,13 +738,13 @@ def extract_nminus1_definition(network_data: NetworkData) -> Nminus1Definition: network_data.simplified_asset_topology if network_data.simplified_asset_topology else network_data.asset_topology ) monitored_nodes = [ - GridElement(id=busbar.grid_model_id, name=busbar.name or "", type=busbar.type, kind="bus") + GridElement(id=busbar.grid_model_id, name=busbar.name or "", type=busbar.busbar_type, kind="bus") for station in asset_topology.materialize_stations() for busbar in station.busbars ] monitored_switches = [ - GridElement(id=switch.grid_model_id, name=switch.name or "", type=switch.type, kind="switch") + GridElement(id=switch.grid_model_id, name=switch.name or "", type=switch.coupler_type, kind="switch") for station in asset_topology.materialize_stations() for switch in station.couplers ] diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py index 3648838f4..7037a99b8 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py @@ -19,12 +19,35 @@ OutageData, get_relevant_stations, ) -from toop_engine_interfaces.asset_topology import MaterializedStation, RawStation, SwitchableAsset -from toop_engine_interfaces.asset_topology_helpers import find_station_by_id, get_connected_assets +from toop_engine_interfaces.asset_topology import ( + BranchAsset, + InjectionAsset, + MaterializedStation, + RawStation, + SwitchableAsset, +) +from toop_engine_interfaces.asset_topology_helpers import find_station_by_id logger = structlog.get_logger(__name__) +def _topology_assets(network_data: NetworkData) -> list[SwitchableAsset]: + """Get the list of assets in the topology. + + Parameters + ---------- + network_data : NetworkData + The network data containing the asset topology. + + Returns + ------- + list[SwitchableAsset] + A list of switchable assets in the topology. + """ + topology = network_data.simplified_asset_topology or network_data.asset_topology + return [*topology.branch_assets, *topology.injection_assets] + + def get_total_injection_along_stub_branch( stub_branch_index: int, current_nodal_index: int, network_data: NetworkData ) -> Float[np.ndarray, " n_timestep"]: @@ -147,7 +170,7 @@ def extract_outage_index_injection_from_asset( branch_index_to_outage = None if asset.in_service: - if asset.is_branch(): + if isinstance(asset, BranchAsset): # Branch is a line or a trafo try: branch_index = network.branch_ids.index(asset.grid_model_id) @@ -168,7 +191,7 @@ def extract_outage_index_injection_from_asset( branch_index, nodal_index_for_busbar, network ) stub_power_map[key] = nodal_injection_to_outage - else: + elif isinstance(asset, InjectionAsset): # Branch is an injection try: injection_index = network.injection_ids.index(asset.grid_model_id) @@ -206,7 +229,7 @@ def get_busbar_map_adjacent_branches(network_data: NetworkData) -> Bool[np.ndarr for station_id in network_data.busbar_outage_map.keys(): # Find the asset topo station id station = find_station_by_id(network_data.asset_topology.materialize_stations(), station_id) - for asset_connection in station.asset_connections: + for asset_connection in station.branch_connections: bb_outage_asset_indices.add(asset_connection.asset.grid_model_id) busbar_outage_branch_mask = np.array([(id in bb_outage_asset_indices) for id in network_data.branch_ids]) @@ -238,9 +261,16 @@ def get_busbar_branches_map(station: MaterializedStation, network_data: NetworkD - The branch indices are determined based on their position in the `network_data.branch_ids` list. """ busbar_branches_map = {} + topology_assets = _topology_assets(network_data) for bb_index, bb in enumerate(station.busbars): - connected_assets = get_connected_assets(station, bb_index) - connected_branches = [asset.grid_model_id for asset in connected_assets if asset.is_branch() and asset.in_service] + connected_branches = [ + asset.grid_model_id + for asset in station.get_connected_assets( + bb_index, + topology_assets=topology_assets, + asset_scope="branch", + ) + ] connected_branches = [network_data.branch_ids.index(branch_id) for branch_id in connected_branches] busbar_branches_map[bb.grid_model_id] = connected_branches return busbar_branches_map @@ -329,7 +359,10 @@ def extract_busbar_outage_data( assert station.busbars[busbar_index].in_service, f"Busbar {busbar_id} is not in service. Cannot outage it." connected_branches_to_outage = [] - connected_assets = get_connected_assets(station, busbar_index) + connected_assets = station.get_connected_assets( + busbar_index, + topology_assets=_topology_assets(network), + ) node_indices_to_outage = np.nonzero(np.array(network.node_ids) == station.grid_model_id)[0].tolist() # Determine the nodal_index of the physical busbar. diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py index 1050af6a5..21040aa15 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py @@ -20,7 +20,6 @@ get_relevant_stations, ) from toop_engine_interfaces.asset_topology import RawStation -from toop_engine_interfaces.asset_topology_helpers import get_connected_assets from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits @@ -129,10 +128,12 @@ def get_injections_on_physical_bb( Float[np.ndarray, " n_timesteps"] The total connected injections in MW for the specified busbar index over all timesteps. """ - connected_assets = get_connected_assets(sub, busbar_index, network_data.simplified_asset_topology.assets) - connected_injection_ids = [ - asset.grid_model_id for asset in connected_assets if asset.in_service and not asset.is_branch() - ] + connected_assets = sub.get_connected_assets( + busbar_index, + topology_assets=network_data.simplified_asset_topology.injection_assets, + asset_scope="injection", + ) + connected_injection_ids = [asset.grid_model_id for asset in connected_assets if asset.in_service] # Certain IDs in the connected_injection_ids may not be present in the network_data.injection_ids. # TODO: FInd out why? diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py index 13f4459cd..9f9c860eb 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py @@ -12,6 +12,7 @@ import itertools from dataclasses import dataclass +from typing import get_args import jax.numpy as jnp import networkx as nx @@ -25,8 +26,11 @@ ) from toop_engine_dc_solver.preprocess.helpers.switching_distance import hamming_distance from toop_engine_interfaces.asset_topology import ( + AssetInjectionType, + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, MaterializedStation, SwitchableAsset, Topology, @@ -124,13 +128,13 @@ def make_separation_set( """ assert all(busbar.in_service for busbar in station.busbars) assert all(coupler.in_service for coupler in station.couplers) - assert all(asset_connection.asset.in_service for asset_connection in station.asset_connections) + assert all(asset_connection.asset.in_service for asset_connection in station.branch_connections) if not len(station.couplers): - return (np.zeros((0, 2, len(station.asset_connections)), dtype=bool), np.zeros((0, 0), dtype=bool), []) + return (np.zeros((0, 2, len(station.branch_connections)), dtype=bool), np.zeros((0, 0), dtype=bool), []) # Go through all possible coupler assignments and find all the configurations in which there are # exactly two electrical busbars. switching_map = { - busbar_index: station.asset_switching_table[busbar_index] for busbar_index, _ in enumerate(station.busbars) + busbar_index: station.branch_switching_table[busbar_index] for busbar_index, _ in enumerate(station.busbars) } busbar_int_id_map = {busbar.int_id: busbar_index for busbar_index, busbar in enumerate(station.busbars)} configurations = [] @@ -425,26 +429,32 @@ def add_missing_asset_topology_branch_info( branch_id_lookup = {branch_id: i for i, branch_id in enumerate(branch_ids)} updated_assets_by_id: dict[str, SwitchableAsset] = {} - for asset in asset_topology.assets: + for asset in asset_topology.branch_assets: index = branch_id_lookup.get(asset.grid_model_id, None) if index is not None: - updated_assets_by_id[asset.grid_model_id] = asset.model_copy( - update={ - "name": (branch_names[index] if overwrite_if_present or asset.name is None else asset.name), - "type": (branch_types[index] if overwrite_if_present or asset.type is None else asset.type), - } + asset_data = asset.model_dump() + asset_data["name"] = branch_names[index] if overwrite_if_present or asset.name is None else asset.name + asset_data["asset_type"] = ( + branch_types[index] if overwrite_if_present or asset.asset_type is None else asset.asset_type ) + updated_assets_by_id[asset.grid_model_id] = BranchAsset(**asset_data) else: - updated_assets_by_id[asset.grid_model_id] = asset + updated_assets_by_id[asset.grid_model_id] = asset.model_copy(deep=True) + + updated_injection_assets = [asset.model_copy(deep=True) for asset in asset_topology.injection_assets] new_raw_stations = [] for station in asset_topology.raw_stations: new_branch_ends = [] - for asset_connection in station.asset_connections: + for asset_connection in station.branch_connections: asset_id = asset_connection.asset_id branch_end = asset_connection.terminal index = branch_id_lookup.get(asset_id, None) - if index is not None and (overwrite_if_present or branch_end is None): + if ( + index is not None + and isinstance(updated_assets_by_id[asset_id], BranchAsset) + and (overwrite_if_present or branch_end is None) + ): new_branch_ends.append("from" if branch_from_nodes[index] == station.grid_model_id else "to") else: new_branch_ends.append(branch_end) @@ -453,8 +463,9 @@ def add_missing_asset_topology_branch_info( return copy_topology_with_updates( reference_topology=asset_topology, raw_stations=new_raw_stations, - assets=[updated_assets_by_id[asset.grid_model_id] for asset in asset_topology.assets], asset_bays=asset_topology.asset_bays, + branch_assets=[updated_assets_by_id[asset.grid_model_id] for asset in asset_topology.branch_assets], + injection_assets=updated_injection_assets, ) @@ -492,24 +503,28 @@ def add_missing_asset_topology_injection_info( # Faster lookup of the position of the injection injection_id_lookup = {injection_id: i for i, injection_id in enumerate(injection_ids)} - updated_assets = [] - for asset in asset_topology.assets: + updated_branch_assets = [asset.model_copy(deep=True) for asset in asset_topology.branch_assets] + updated_injection_assets = [] + for asset in asset_topology.injection_assets: index = injection_id_lookup.get(asset.grid_model_id, None) if index is not None: - updated_assets.append( - asset.model_copy( - update={ - "name": (injection_names[index] if overwrite_if_present or asset.name is None else asset.name), - "type": (injection_types[index] if overwrite_if_present or asset.type is None else asset.type), - } - ) - ) + asset_data = asset.model_dump() + asset_data["name"] = injection_names[index] if overwrite_if_present or asset.name is None else asset.name + updated_type = injection_types[index] if overwrite_if_present or asset.asset_type is None else asset.asset_type + if updated_type in get_args(AssetInjectionType): + asset_data["asset_type"] = updated_type + updated_injection_assets.append(InjectionAsset(**asset_data)) + elif isinstance(asset, BranchAsset): + updated_injection_assets.append(BranchAsset(**asset_data)) + else: + updated_injection_assets.append(asset.model_copy(update=asset_data, deep=True)) else: - updated_assets.append(asset) + updated_injection_assets.append(asset.model_copy(deep=True)) return copy_topology_with_updates( reference_topology=asset_topology, raw_stations=asset_topology.raw_stations, - assets=updated_assets, asset_bays=asset_topology.asset_bays, + branch_assets=updated_branch_assets, + injection_assets=updated_injection_assets, ) diff --git a/packages/dc_solver_pkg/tests/conftest.py b/packages/dc_solver_pkg/tests/conftest.py index 04561fb03..d35d6c433 100644 --- a/packages/dc_solver_pkg/tests/conftest.py +++ b/packages/dc_solver_pkg/tests/conftest.py @@ -95,11 +95,11 @@ from toop_engine_grid_helpers.powsybl.powsybl_helpers import save_lf_params_to_fs from toop_engine_importer.pypowsybl_import import preprocessing from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, MaterializedAssetConnection, MaterializedStation, - SwitchableAsset, Topology, ) from toop_engine_interfaces.asset_topology_helpers import load_asset_topology @@ -843,10 +843,10 @@ def basic_node_breaker_topology() -> Topology: @pytest.fixture(scope="session") def mock_station() -> MaterializedStation: - asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, type="line") - asset2 = SwitchableAsset(grid_model_id="branch_02", in_service=True, type="line") - asset3 = SwitchableAsset(grid_model_id="branch_03", in_service=True, type="line") - asset4 = SwitchableAsset(grid_model_id="branch_04", in_service=True, type="line") + asset1 = BranchAsset(grid_model_id="branch_01", in_service=True, asset_type="line") + asset2 = BranchAsset(grid_model_id="branch_02", in_service=True, asset_type="line") + asset3 = BranchAsset(grid_model_id="branch_03", in_service=True, asset_type="line") + asset4 = BranchAsset(grid_model_id="branch_04", in_service=True, asset_type="line") # Create mock Busbar objects busbar_0 = Busbar(grid_model_id="busbar_0", int_id=1) @@ -866,7 +866,7 @@ def mock_station() -> MaterializedStation: couplers=[ BusbarCoupler( grid_model_id="VL4_BREAKER", - type="busbar_coupler", + coupler_type="busbar_coupler", name="VL4_BREAKER", busbar_from_id=1, busbar_to_id=2, @@ -875,7 +875,7 @@ def mock_station() -> MaterializedStation: ), BusbarCoupler( grid_model_id="VL5_BREAKER", - type="busbar_coupler", + coupler_type="busbar_coupler", name="VL5_BREAKER", busbar_from_id=2, busbar_to_id=3, @@ -884,7 +884,7 @@ def mock_station() -> MaterializedStation: ), BusbarCoupler( grid_model_id="VL6_BREAKER", - type="busbar_coupler", + coupler_type="busbar_coupler", name="VL6_BREAKER", busbar_from_id=3, busbar_to_id=4, @@ -893,7 +893,7 @@ def mock_station() -> MaterializedStation: ), BusbarCoupler( grid_model_id="VL7_BREAKER", - type="busbar_coupler", + coupler_type="busbar_coupler", name="VL7_BREAKER", busbar_from_id=4, busbar_to_id=5, @@ -902,7 +902,7 @@ def mock_station() -> MaterializedStation: ), BusbarCoupler( grid_model_id="VL9_BREAKER", - type="busbar_coupler", + coupler_type="busbar_coupler", name="VL9_BREAKER", busbar_from_id=1, busbar_to_id=3, @@ -910,13 +910,14 @@ def mock_station() -> MaterializedStation: in_service=True, ), ], - asset_connections=[ + branch_connections=[ MaterializedAssetConnection(asset=asset1), MaterializedAssetConnection(asset=asset2), MaterializedAssetConnection(asset=asset3), MaterializedAssetConnection(asset=asset4), ], - asset_switching_table=np.array( + injection_connections=[], + branch_switching_table=np.array( [ [True, False, True, False], # Busbar 0 [False, True, False, False], # Busbar 1 @@ -926,6 +927,7 @@ def mock_station() -> MaterializedStation: ], dtype=bool, ), + injection_switching_table=np.zeros((5, 0), dtype=bool), ) return station diff --git a/packages/dc_solver_pkg/tests/deprecated/assignment.py b/packages/dc_solver_pkg/tests/deprecated/assignment.py index 021315bb9..a1ffbc616 100644 --- a/packages/dc_solver_pkg/tests/deprecated/assignment.py +++ b/packages/dc_solver_pkg/tests/deprecated/assignment.py @@ -250,7 +250,7 @@ def realize_single_station_assignment( busbar_a, busbar_b, asset_index=asset_i, - asset_connectivity=station.asset_connectivity, + asset_connectivity=np.concatenate([station.branch_connectivity, station.injection_connectivity], axis=1), ) # break when the branch action is not feasible diff --git a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py index b6ba11713..08f2d91d1 100644 --- a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py +++ b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py @@ -13,9 +13,7 @@ import numpy as np import pandas as pd import pandera -import pandera.typing as pat import pytest -from beartype.typing import cast from toop_engine_dc_solver.export.asset_topology_to_dgs import ( ForeignIdSchema, get_dgs_general_schema, @@ -29,7 +27,6 @@ DgsElmCoupSchema, ) from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import ( - get_asset_bay_grid_model_id_list, get_asset_switch_states_from_station, get_busbar_lookup, get_changing_switches_from_topology, @@ -76,14 +73,13 @@ def test_dgs_list_to_xlsx(tmp_path): ] ) DgsElmCoupSchema.validate(dgs_df) - typed_dgs_df = cast(pat.DataFrame[DgsElmCoupSchema], dgs_df) file_name = os.path.join(tmp_path, "test_dgs.xlsx") assert not os.path.exists(file_name) sheet_name = "ElmCoup" general_info = DGS_GENERAL_SHEET_CONTENT_FID general_df = get_dgs_general_schema(general_info=general_info, cim=False) switch_dgs_schema_to_xlsx( - switch_dgs_schema=typed_dgs_df, + switch_dgs_schema=dgs_df, file_name=file_name, sheet_name=sheet_name, df_general=general_df, @@ -101,7 +97,7 @@ def test_dgs_list_to_xlsx(tmp_path): # default general info file_name = os.path.join(tmp_path, "test2_dgs.xlsx") switch_dgs_schema_to_xlsx( - switch_dgs_schema=typed_dgs_df, + switch_dgs_schema=dgs_df, file_name=file_name, sheet_name=sheet_name, df_general=general_df, @@ -127,7 +123,7 @@ def test_get_coupler_states_from_busbar_couplers(): mock_coupler_2.open = False mock_coupler_2.in_service = True - busbar_couplers = cast(list[BusbarCoupler], [mock_coupler_1, mock_coupler_2]) + busbar_couplers = [mock_coupler_1, mock_coupler_2] result = get_coupler_states_from_busbar_couplers(busbar_couplers) @@ -156,7 +152,7 @@ def test_get_asset_switch_states_from_station(basic_node_breaker_topology): # test empty disconnection station = station.model_copy( update={ - "asset_switching_table": np.array([[False, False, False], [True, True, True]]), + "branch_switching_table": np.array([[False, False, False], [True, True, True]]), } ) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) @@ -178,7 +174,7 @@ def test_get_asset_switch_states_from_station(basic_node_breaker_topology): assert switch_reassignment_df.to_dict(orient="records") == expected_reassignment # test empty reassignment - station = station.model_copy(update={"asset_switching_table": np.array([[False, False, False], [False, False, False]])}) + station = station.model_copy(update={"branch_switching_table": np.array([[False, False, False], [False, False, False]])}) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) expected_disconnection = [ {"grid_model_id": "L42_BREAKER", "open": True}, @@ -191,7 +187,10 @@ def test_get_asset_switch_states_from_station(basic_node_breaker_topology): def test_get_asset_bay_sr_fid_list(basic_node_breaker_topology): station = deepcopy(basic_node_breaker_topology.materialize_stations()[0]) - asset_bay_sr_fid_list = get_asset_bay_grid_model_id_list(station) + asset_bay_sr_fid_list = [ + *(asset_connection.get_sr_switch() for asset_connection in station.branch_connections), + *(asset_connection.get_sr_switch() for asset_connection in station.injection_connections), + ] expected = [ {"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, {"BBS4_1": "L52_DISCONNECTOR_5_0", "BBS4_2": "L52_DISCONNECTOR_5_1"}, @@ -201,14 +200,17 @@ def test_get_asset_bay_sr_fid_list(basic_node_breaker_topology): station = station.model_copy( update={ - "asset_connections": [ - station.asset_connections[0], - station.asset_connections[1].model_copy(update={"asset_bay": None}), - station.asset_connections[2], + "branch_connections": [ + station.branch_connections[0], + station.branch_connections[1].model_copy(update={"asset_bay": None}), + station.branch_connections[2], ] } ) - asset_bay_sr_fid_list = get_asset_bay_grid_model_id_list(station) + asset_bay_sr_fid_list = [ + *(asset_connection.get_sr_switch() for asset_connection in station.branch_connections), + *(asset_connection.get_sr_switch() for asset_connection in station.injection_connections), + ] expected = [ {"BBS4_1": "L42_DISCONNECTOR_3_0", "BBS4_2": "L42_DISCONNECTOR_3_1"}, None, @@ -228,10 +230,7 @@ def test_station_helpers_build_switch_update_schema(basic_node_breaker_topology) station = basic_node_breaker_topology.materialize_stations()[0] coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) - switch_update_schema = cast( - pat.DataFrame[SwitchUpdateSchema], - pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True), - ) + switch_update_schema = pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True) expected = pd.DataFrame( [ {"grid_model_id": "VL4_BREAKER", "open": True}, @@ -250,10 +249,7 @@ def test_get_diff_switch_states(basic_node_breaker_grid_v1, basic_node_breaker_t station = basic_node_breaker_topology.materialize_stations()[0] coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) - switch_update_schema = cast( - pat.DataFrame[SwitchUpdateSchema], - pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True), - ) + switch_update_schema = pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True) diff_switch_states = get_diff_switch_states(network=net, switch_df=switch_update_schema) SwitchUpdateSchema.validate(diff_switch_states) expected = [ @@ -284,15 +280,12 @@ def test_switch_update_schema_to_dgs(basic_node_breaker_grid_v1, basic_node_brea station = basic_node_breaker_topology.materialize_stations()[0] coupler_df = get_coupler_states_from_busbar_couplers(station.couplers) switch_reassignment_df, switch_disconnection_df = get_asset_switch_states_from_station(station) - switch_update_schema = cast( - pat.DataFrame[SwitchUpdateSchema], - pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True), - ) + switch_update_schema = pd.concat([coupler_df, switch_reassignment_df, switch_disconnection_df], ignore_index=True) foreign_ids = deepcopy(switch_update_schema) foreign_ids["foreign_id"] = foreign_ids["grid_model_id"] + "_foreign_id" foreign_ids.drop(columns=["open"], inplace=True) ForeignIdSchema.validate(foreign_ids) - typed_foreign_ids = cast(pat.DataFrame[ForeignIdSchema], foreign_ids) + typed_foreign_ids = foreign_ids dgs_df = switch_update_schema_to_dgs(switch_update_schema, typed_foreign_ids, cim=False) # check if dgs switch states are correct @@ -327,11 +320,10 @@ def test_switch_dgs_schema_to_bytes_io(tmp_path): ] ) DgsElmCoupSchema.validate(dgs_df) - typed_dgs_df = cast(pat.DataFrame[DgsElmCoupSchema], dgs_df) general_df = get_dgs_general_schema(cim=False) # Call the function - output = switch_dgs_schema_to_bytes_io(typed_dgs_df, general_df, sheet_name="ElmCoup") + output = switch_dgs_schema_to_bytes_io(dgs_df, general_df, sheet_name="ElmCoup") # Check that output is a BytesIO and not empty assert isinstance(output, io.BytesIO) diff --git a/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py index fcf6b711e..a6fc19656 100644 --- a/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py @@ -37,8 +37,9 @@ def build_test_topology(reference_topology: Topology, raw_stations: list[RawStat return copy_topology_with_updates( reference_topology=reference_topology, raw_stations=raw_stations, - assets=reference_topology.assets, asset_bays=reference_topology.asset_bays, + branch_assets=reference_topology.branch_assets, + injection_assets=reference_topology.injection_assets, ) @@ -91,14 +92,14 @@ def test_get_changing_switches_from_disconnections_matches_network_diff( starting_station = target_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_station.couplers], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) target_raw_station = basic_node_breaker_topology.raw_stations[0] starting_raw_station = target_raw_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_raw_station.couplers], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) target_topology = build_test_topology(basic_node_breaker_topology, [target_raw_station]) @@ -122,13 +123,13 @@ def test_get_changing_switches_from_actions_warns_on_overlapping_switch_updates( target_station = basic_node_breaker_topology.materialize_stations()[0] changed_station = target_station.model_copy( update={ - "asset_switching_table": np.array([[False, False, False], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[False, False, False], [True, True, False]], dtype=bool), } ) starting_station = target_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_station.couplers], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) starting_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( @@ -137,7 +138,7 @@ def test_get_changing_switches_from_actions_warns_on_overlapping_switch_updates( coupler.model_copy(update={"open": False}) for coupler in basic_node_breaker_topology.raw_stations[0].couplers ], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) starting_topology = build_test_topology(basic_node_breaker_topology, [starting_raw_station]) diff --git a/packages/dc_solver_pkg/tests/export/test_export.py b/packages/dc_solver_pkg/tests/export/test_export.py index 1a6d095a9..360786109 100644 --- a/packages/dc_solver_pkg/tests/export/test_export.py +++ b/packages/dc_solver_pkg/tests/export/test_export.py @@ -46,8 +46,9 @@ def build_test_topology(reference_topology: Topology, raw_stations: list[RawStat return copy_topology_with_updates( reference_topology=reference_topology, raw_stations=raw_stations, - assets=reference_topology.assets, asset_bays=reference_topology.asset_bays, + branch_assets=reference_topology.branch_assets, + injection_assets=reference_topology.injection_assets, ) diff --git a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py index d4bcbd005..0564ca267 100644 --- a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py @@ -7,6 +7,7 @@ import numpy as np import pytest +from pydantic import ValidationError from toop_engine_dc_solver.export.station_switch_updates import ( _get_asset_switch_diffs, _get_coupler_switch_diffs, @@ -36,8 +37,9 @@ def build_test_topology(reference_topology: Topology, raw_stations: list[RawStat return copy_topology_with_updates( reference_topology=reference_topology, raw_stations=raw_stations, - assets=reference_topology.assets, asset_bays=reference_topology.asset_bays, + branch_assets=reference_topology.branch_assets, + injection_assets=reference_topology.injection_assets, ) @@ -73,12 +75,12 @@ def test_get_asset_switch_diffs(basic_node_breaker_topology): target_station = basic_node_breaker_topology.materialize_stations()[0] starting_station = target_station.model_copy( update={ - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) changed_station = target_station.model_copy( update={ - "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) @@ -95,23 +97,22 @@ def test_get_asset_switch_diffs(basic_node_breaker_topology): def test_get_asset_switch_diffs_requires_matching_switching_table_shape(basic_node_breaker_topology): station = basic_node_breaker_topology.materialize_stations()[0] - changed_station = station.model_copy( - update={ - "asset_switching_table": np.array([[False, False, True]], dtype=bool), - } - ) - - with pytest.raises(ValueError, match="asset switching table shape does not match"): - _get_asset_switch_diffs( - changed_station=changed_station, - starting_station=station, + with pytest.raises(ValidationError, match="branch_switching_table shape"): + station.model_copy( + update={ + "branch_switching_table": np.array([[False, False, True]], dtype=bool), + } ) def test_get_asset_switch_diffs_requires_matching_asset_order(basic_node_breaker_topology): station = basic_node_breaker_topology.materialize_stations()[0] - reordered_asset_connections = [station.asset_connections[1], station.asset_connections[0], station.asset_connections[2]] - changed_station = station.model_copy(update={"asset_connections": reordered_asset_connections}) + reordered_asset_connections = [ + station.branch_connections[1], + station.branch_connections[0], + station.branch_connections[2], + ] + changed_station = station.model_copy(update={"branch_connections": reordered_asset_connections}) with pytest.raises(ValueError, match="Use ActionSet.simplified_starting_topology as input"): _get_asset_switch_diffs( @@ -124,12 +125,12 @@ def test_get_asset_switch_diffs_allows_multiple_active_busbars(basic_node_breake station = basic_node_breaker_topology.materialize_stations()[0] starting_station = station.model_copy( update={ - "asset_switching_table": np.array([[True, False, False], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, False], [True, True, False]], dtype=bool), } ) changed_station = station.model_copy( update={ - "asset_switching_table": np.array([[True, False, False], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, False], [False, True, False]], dtype=bool), } ) @@ -150,13 +151,13 @@ def test_get_changing_switches_from_changed_stations_matches_network_diff( starting_station = station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in station.couplers], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) changed_station = starting_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": True}) for coupler in starting_station.couplers], - "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) target_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( @@ -164,7 +165,7 @@ def test_get_changing_switches_from_changed_stations_matches_network_diff( "couplers": [ coupler.model_copy(update={"open": True}) for coupler in basic_node_breaker_topology.raw_stations[0].couplers ], - "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) starting_raw_station = basic_node_breaker_topology.raw_stations[0].model_copy( @@ -173,7 +174,7 @@ def test_get_changing_switches_from_changed_stations_matches_network_diff( coupler.model_copy(update={"open": False}) for coupler in basic_node_breaker_topology.raw_stations[0].couplers ], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) target_topology = build_test_topology(basic_node_breaker_topology, [target_raw_station]) 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..b2da454df 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,842 @@ -{"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, + "raw_stations": [ + { + "grid_model_id": "VL2_0", + "name": "VLevel2", + "station_type": null, + "region": "BE", + "voltage_level": 225.0, + "busbars": [ + { + "grid_model_id": "BBS2_1", + "busbar_type": "busbar", + "name": "bus1", + "int_id": 0, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + }, + { + "grid_model_id": "BBS2_2", + "busbar_type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + }, + { + "grid_model_id": "BBS2_3", + "busbar_type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + } + ], + "couplers": [ + { + "grid_model_id": "VL2_BREAKER", + "coupler_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", + "coupler_type": "BREAKER", + "name": "VL2_BREAKER#0", + "busbar_from_id": 1, + "busbar_to_id": 2, + "open": false, + "in_service": true, + "asset_bay": null + } + ], + "branch_switching_table": [ + [ + true, + false, + true, + false, + true + ], + [ + false, + true, + false, + true, + false + ], + [ + false, + false, + false, + false, + false + ] + ], + "injection_switching_table": [ + [ + true + ], + [ + false + ], + [ + false + ] + ], + "branch_connectivity": [ + [ + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true + ] + ], + "injection_connectivity": [ + [ + true + ], + [ + true + ], + [ + true + ] + ], + "model_log": [], + "branch_connections": [ + { + "asset_id": "L1", + "terminal": "to", + "asset_bay_id": "VLevel2::L1::bay" + }, + { + "asset_id": "L2", + "terminal": "to", + "asset_bay_id": "VLevel2::L2::bay" + }, + { + "asset_id": "L6", + "terminal": "from", + "asset_bay_id": "VLevel2::L6::bay" + }, + { + "asset_id": "L7", + "terminal": "from", + "asset_bay_id": "VLevel2::L7::bay" + }, + { + "asset_id": "L8", + "terminal": "from", + "asset_bay_id": "VLevel2::L8::bay" + } + ], + "injection_connections": [ + { + "asset_id": "load1", + "terminal": null, + "asset_bay_id": "VLevel2::load1::bay" + } + ] + }, + { + "grid_model_id": "VL3_0", + "name": "VLevel3", + "station_type": null, + "region": "BE", + "voltage_level": 225.0, + "busbars": [ + { + "grid_model_id": "BBS3_1", + "busbar_type": "busbar", + "name": "bus1", + "int_id": 0, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + }, + { + "grid_model_id": "BBS3_2", + "busbar_type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + }, + { + "grid_model_id": "BBS3_3", + "busbar_type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + }, + { + "grid_model_id": "BBS3_4", + "busbar_type": "busbar", + "name": "bus4", + "int_id": 3, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + } + ], + "couplers": [ + { + "grid_model_id": "VL3_BREAKER", + "coupler_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", + "coupler_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", + "coupler_type": "BREAKER", + "name": "VL3_BREAKER#1", + "busbar_from_id": 1, + "busbar_to_id": 3, + "open": false, + "in_service": true, + "asset_bay": null + } + ], + "branch_switching_table": [ + [ + true, + true, + false, + true + ], + [ + false, + false, + true, + false + ], + [ + false, + false, + false, + false + ], + [ + false, + false, + false, + false + ] + ], + "injection_switching_table": [ + [ + false + ], + [ + true + ], + [ + false + ], + [ + false + ] + ], + "branch_connectivity": [ + [ + true, + true, + true, + true + ], + [ + true, + true, + true, + true + ], + [ + false, + false, + false, + false + ], + [ + false, + false, + false, + false + ] + ], + "injection_connectivity": [ + [ + true + ], + [ + true + ], + [ + false + ], + [ + false + ] + ], + "model_log": [], + "branch_connections": [ + { + "asset_id": "L3", + "terminal": "to", + "asset_bay_id": "VLevel3::L3::bay" + }, + { + "asset_id": "L6", + "terminal": "to", + "asset_bay_id": "VLevel3::L6::bay" + }, + { + "asset_id": "L7", + "terminal": "to", + "asset_bay_id": "VLevel3::L7::bay" + }, + { + "asset_id": "L9", + "terminal": null, + "asset_bay_id": "VLevel3::L9::bay" + } + ], + "injection_connections": [ + { + "asset_id": "load2", + "terminal": null, + "asset_bay_id": "VLevel3::load2::bay" + } + ] + } + ], + "branch_assets": [ + { + "grid_model_id": "L1", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L2", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L6", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L7", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L8", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L3", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L9", + "asset_type": "LINE", + "name": "", + "in_service": true + } + ], + "injection_assets": [ + { + "grid_model_id": "load1", + "asset_type": "LOAD", + "name": "", + "in_service": true + }, + { + "grid_model_id": "load2", + "asset_type": "LOAD", + "name": "", + "in_service": true + } + ], + "asset_bays": [ + { + "asset_bay_id": "VLevel2::L1::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" + } + }, + { + "asset_bay_id": "VLevel2::L2::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" + } + }, + { + "asset_bay_id": "VLevel2::L6::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" + } + }, + { + "asset_bay_id": "VLevel2::L7::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" + } + }, + { + "asset_bay_id": "VLevel2::L8::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" + } + }, + { + "asset_bay_id": "VLevel2::load1::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_bay_id": "VLevel3::L3::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L32_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L32_DISCONNECTOR_5_0", + "BBS3_2": "L32_DISCONNECTOR_5_1" + } + }, + { + "asset_bay_id": "VLevel3::L6::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L62_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L62_DISCONNECTOR_7_0", + "BBS3_2": "L62_DISCONNECTOR_7_1" + } + }, + { + "asset_bay_id": "VLevel3::L7::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L72_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L72_DISCONNECTOR_9_0", + "BBS3_2": "L72_DISCONNECTOR_9_1" + } + }, + { + "asset_bay_id": "VLevel3::L9::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L91_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L91_DISCONNECTOR_11_0", + "BBS3_2": "L91_DISCONNECTOR_11_1" + } + }, + { + "asset_bay_id": "VLevel3::load2::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "load2_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "load2_DISCONNECTOR_19_0", + "BBS3_2": "load2_DISCONNECTOR_19_1" + } + } + ], + "asset_setpoints": null, + "timestamp": "2026-06-18T21:43:40.631530", + "metrics": null + }, + "simplified_starting_topology": { + "topology_id": "grid.xiidm", + "grid_model_file": "grid.xiidm", + "name": null, + "raw_stations": [ + { + "grid_model_id": "VL2_0", + "name": "VLevel2", + "station_type": null, + "region": "BE", + "voltage_level": 225.0, + "busbars": [ + { + "grid_model_id": "BBS2_1", + "busbar_type": "busbar", + "name": "bus1", + "int_id": 0, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + }, + { + "grid_model_id": "BBS2_2", + "busbar_type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + }, + { + "grid_model_id": "BBS2_3", + "busbar_type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + } + ], + "couplers": [ + { + "grid_model_id": "VL2_BREAKER", + "coupler_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", + "coupler_type": "BREAKER", + "name": "VL2_BREAKER#0", + "busbar_from_id": 1, + "busbar_to_id": 2, + "open": false, + "in_service": true, + "asset_bay": null + } + ], + "branch_switching_table": [ + [ + true, + false, + true, + true, + false + ], + [ + false, + true, + false, + false, + true + ], + [ + false, + false, + false, + false, + false + ] + ], + "injection_switching_table": [ + [ + true + ], + [ + false + ], + [ + false + ] + ], + "branch_connectivity": [ + [ + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true + ] + ], + "injection_connectivity": [ + [ + true + ], + [ + true + ], + [ + true + ] + ], + "model_log": [], + "branch_connections": [ + { + "asset_id": "L6", + "terminal": "from", + "asset_bay_id": "VLevel2::L6::bay" + }, + { + "asset_id": "L7", + "terminal": "from", + "asset_bay_id": "VLevel2::L7::bay" + }, + { + "asset_id": "L8", + "terminal": "from", + "asset_bay_id": "VLevel2::L8::bay" + }, + { + "asset_id": "L1", + "terminal": "to", + "asset_bay_id": "VLevel2::L1::bay" + }, + { + "asset_id": "L2", + "terminal": "to", + "asset_bay_id": "VLevel2::L2::bay" + } + ], + "injection_connections": [ + { + "asset_id": "load1", + "terminal": null, + "asset_bay_id": "VLevel2::load1::bay" + } + ] + } + ], + "branch_assets": [ + { + "grid_model_id": "L6", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L7", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L8", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L1", + "asset_type": "LINE", + "name": "", + "in_service": true + }, + { + "grid_model_id": "L2", + "asset_type": "LINE", + "name": "", + "in_service": true + } + ], + "injection_assets": [ + { + "grid_model_id": "load1", + "asset_type": "LOAD", + "name": "", + "in_service": true + } + ], + "asset_bays": [ + { + "asset_bay_id": "VLevel2::L6::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" + } + }, + { + "asset_bay_id": "VLevel2::L7::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" + } + }, + { + "asset_bay_id": "VLevel2::L8::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" + } + }, + { + "asset_bay_id": "VLevel2::L1::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" + } + }, + { + "asset_bay_id": "VLevel2::L2::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" + } + }, + { + "asset_bay_id": "VLevel2::load1::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_setpoints": null, + "timestamp": "2026-06-18T21:43:40.631530", + "metrics": null + }, + "connectable_branches": [], + "disconnectable_branches": [ + { + "id": "L1", + "name": "VL1_2 ## VL2_3 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L2", + "name": "VL1_4 ## VL2_5 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L3", + "name": "VL1_6 ## VL3_4 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L4", + "name": "VL1_8 ## VL4_4 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L5", + "name": "VL1_10 ## VL4_6 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L6", + "name": "VL2_7 ## VL3_6 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L7", + "name": "VL2_9 ## VL3_8 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L8", + "name": "VL2_11 ## VL4_8 ## ", + "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 0000000000000000000000000000000000000000..54cf998e0e9a2282550617f973ef060f87d17be5 GIT binary patch literal 7568 zcmeHMy-piJ5T5nL(g{Ig1mQ0vN|`dEqD8^r!UDvf=nmtYVn^tV4oC0^q@zNVl$3}k zKvX;eC67Uu%FfiK)wmFS0w+(fDMOYUtg_O0V6fPN^!&17|%jAn0!$P)T(nU zRPdHmtt`C}o+%ic^_l21D$2*2I{r}(3t#iR9+Z_hcnzb-?)`3~+wLS$C;b|y_^e6i z{GGH;UJEnVp`C`VPh}Zxr}CX3+v1nyy7x(;kK^cmt&`?7Zr`x9t07Thglo8a%kpo1vkOT;JrNr(4*~6)T}bIG zBCn_Wi^bZ~@;otE`?aUSTkT`zdEDRCJm2gN%G@gy;2eDN#kdk1xN#4lZP2_PV7xy^ zFo0wQH-zsDVPUSmhD~=^U)><#9cA1x@|Q~dfKX1Lz}-*!80>atM=PuLSz}%W59i|G zRPh6qe`n*xae6*pzAKLVf4uw)lfCA-HtWlE0W(IqC2eYCCXYYad&j+L*_D$V{Q5Q^ zXYP@T*kIxO!Iav%_umiiV^}mM8;nu(=Z8)umA7Mb_I6S=KwlDY#KSVY`t(F7-X+Na z`CU?b?{+DbcZ45u`1g00^kL1;kGku+P;{z1FjY_*ebQMa+P8w0eH Y?{U+=t9jz`sT7xOul4?d`3pY30Z0vd_W%F@ literal 0 HcmV?d00001 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..dec263039 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,373 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..5053bdc82 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,268 @@ -{ - "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", + "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" + ], + "trafo_disconnectable": [] + }, + "import_result": { + "data_folder": "../data/test_grid_node_breaker/grid", + "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": "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/test_grid_node_breaker/grid", + "grid_model_file": "../data/test_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 + } + } +} 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 e7a4a8fd4..79a08e320 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,501 +1,516 @@ { - "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, - "raw_stations": [ - { - "grid_model_id": "VL2_0", - "name": "VLevel2", - "type": null, - "region": "BE", - "voltage_level": 225.0, - "busbars": [ + "topology_id": "grid.xiidm", + "grid_model_file": "grid.xiidm", + "name": null, + "raw_stations": [ { - "grid_model_id": "BBS2_1", - "type": "busbar", - "name": "bus1", - "int_id": 0, - "in_service": true, - "bus_branch_bus_id": null + "grid_model_id": "VL2_0", + "name": "VLevel2", + "station_type": null, + "region": "BE", + "voltage_level": 225.0, + "busbars": [ + { + "grid_model_id": "BBS2_1", + "busbar_type": "busbar", + "name": "bus1", + "int_id": 0, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + }, + { + "grid_model_id": "BBS2_2", + "busbar_type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + }, + { + "grid_model_id": "BBS2_3", + "busbar_type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": "VL2_0" + } + ], + "couplers": [ + { + "grid_model_id": "VL2_BREAKER", + "coupler_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", + "coupler_type": "BREAKER", + "name": "VL2_BREAKER#0", + "busbar_from_id": 1, + "busbar_to_id": 2, + "open": false, + "in_service": true, + "asset_bay": null + } + ], + "branch_switching_table": [ + [ + true, + false, + true, + false, + true + ], + [ + false, + true, + false, + true, + false + ], + [ + false, + false, + false, + false, + false + ] + ], + "injection_switching_table": [ + [ + true + ], + [ + false + ], + [ + false + ] + ], + "branch_connectivity": [ + [ + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true + ] + ], + "injection_connectivity": [ + [ + true + ], + [ + true + ], + [ + true + ] + ], + "model_log": [], + "branch_connections": [ + { + "asset_id": "L1", + "terminal": null, + "asset_bay_id": "VLevel2::L1::bay" + }, + { + "asset_id": "L2", + "terminal": null, + "asset_bay_id": "VLevel2::L2::bay" + }, + { + "asset_id": "L6", + "terminal": null, + "asset_bay_id": "VLevel2::L6::bay" + }, + { + "asset_id": "L7", + "terminal": null, + "asset_bay_id": "VLevel2::L7::bay" + }, + { + "asset_id": "L8", + "terminal": null, + "asset_bay_id": "VLevel2::L8::bay" + } + ], + "injection_connections": [ + { + "asset_id": "load1", + "terminal": null, + "asset_bay_id": "VLevel2::load1::bay" + } + ] }, { - "grid_model_id": "BBS2_2", - "type": "busbar", - "name": "bus2", - "int_id": 1, - "in_service": true, - "bus_branch_bus_id": null - }, - { - "grid_model_id": "BBS2_3", - "type": "busbar", - "name": "bus3", - "int_id": 2, - "in_service": true, - "bus_branch_bus_id": null + "grid_model_id": "VL3_0", + "name": "VLevel3", + "station_type": null, + "region": "BE", + "voltage_level": 225.0, + "busbars": [ + { + "grid_model_id": "BBS3_1", + "busbar_type": "busbar", + "name": "bus1", + "int_id": 0, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + }, + { + "grid_model_id": "BBS3_2", + "busbar_type": "busbar", + "name": "bus2", + "int_id": 1, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + }, + { + "grid_model_id": "BBS3_3", + "busbar_type": "busbar", + "name": "bus3", + "int_id": 2, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + }, + { + "grid_model_id": "BBS3_4", + "busbar_type": "busbar", + "name": "bus4", + "int_id": 3, + "in_service": true, + "bus_branch_bus_id": "VL3_0" + } + ], + "couplers": [ + { + "grid_model_id": "VL3_BREAKER", + "coupler_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", + "coupler_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", + "coupler_type": "BREAKER", + "name": "VL3_BREAKER#1", + "busbar_from_id": 1, + "busbar_to_id": 3, + "open": false, + "in_service": true, + "asset_bay": null + } + ], + "branch_switching_table": [ + [ + true, + true, + false, + true + ], + [ + false, + false, + true, + false + ], + [ + false, + false, + false, + false + ], + [ + false, + false, + false, + false + ] + ], + "injection_switching_table": [ + [ + false + ], + [ + true + ], + [ + false + ], + [ + false + ] + ], + "branch_connectivity": [ + [ + true, + true, + true, + true + ], + [ + true, + true, + true, + true + ], + [ + false, + false, + false, + false + ], + [ + false, + false, + false, + false + ] + ], + "injection_connectivity": [ + [ + true + ], + [ + true + ], + [ + false + ], + [ + false + ] + ], + "model_log": [], + "branch_connections": [ + { + "asset_id": "L3", + "terminal": null, + "asset_bay_id": "VLevel3::L3::bay" + }, + { + "asset_id": "L6", + "terminal": null, + "asset_bay_id": "VLevel3::L6::bay" + }, + { + "asset_id": "L7", + "terminal": null, + "asset_bay_id": "VLevel3::L7::bay" + }, + { + "asset_id": "L9", + "terminal": null, + "asset_bay_id": "VLevel3::L9::bay" + } + ], + "injection_connections": [ + { + "asset_id": "load2", + "terminal": null, + "asset_bay_id": "VLevel3::load2::bay" + } + ] } - ], - "couplers": [ + ], + "branch_assets": [ { - "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": "L1", + "asset_type": "LINE", + "name": "", + "in_service": true }, { - "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 - } - ], - "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": [], - "asset_connections": [ - { - "asset_id": "L1", - "terminal": null, - "asset_bay_id": "VL2_0::L1::bay" + "grid_model_id": "L2", + "asset_type": "LINE", + "name": "", + "in_service": true }, { - "asset_id": "L2", - "terminal": null, - "asset_bay_id": "VL2_0::L2::bay" + "grid_model_id": "L6", + "asset_type": "LINE", + "name": "", + "in_service": true }, { - "asset_id": "L6", - "terminal": null, - "asset_bay_id": "VL2_0::L6::bay" + "grid_model_id": "L7", + "asset_type": "LINE", + "name": "", + "in_service": true }, { - "asset_id": "L7", - "terminal": null, - "asset_bay_id": "VL2_0::L7::bay" + "grid_model_id": "L8", + "asset_type": "LINE", + "name": "", + "in_service": true }, { - "asset_id": "L8", - "terminal": null, - "asset_bay_id": "VL2_0::L8::bay" + "grid_model_id": "L3", + "asset_type": "LINE", + "name": "", + "in_service": true }, { - "asset_id": "load1", - "terminal": null, - "asset_bay_id": "VL2_0::load1::bay" + "grid_model_id": "L9", + "asset_type": "LINE", + "name": "", + "in_service": true } - ] - }, - { - "grid_model_id": "VL3_0", - "name": "VLevel3", - "type": null, - "region": "BE", - "voltage_level": 225.0, - "busbars": [ + ], + "injection_assets": [ { - "grid_model_id": "BBS3_1", - "type": "busbar", - "name": "bus1", - "int_id": 0, - "in_service": true, - "bus_branch_bus_id": null + "grid_model_id": "load1", + "asset_type": "LOAD", + "name": "", + "in_service": true }, { - "grid_model_id": "BBS3_2", - "type": "busbar", - "name": "bus2", - "int_id": 1, - "in_service": true, - "bus_branch_bus_id": null + "grid_model_id": "load2", + "asset_type": "LOAD", + "name": "", + "in_service": true + } + ], + "asset_bays": [ + { + "asset_bay_id": "VLevel2::L1::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": "BBS3_3", - "type": "busbar", - "name": "bus3", - "int_id": 2, - "in_service": true, - "bus_branch_bus_id": null + "asset_bay_id": "VLevel2::L2::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": "BBS3_4", - "type": "busbar", - "name": "bus4", - "int_id": 3, - "in_service": true, - "bus_branch_bus_id": null - } - ], - "couplers": [ + "asset_bay_id": "VLevel2::L6::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": "VL3_BREAKER", - "type": "BREAKER", - "name": "VL3_BREAKER", - "busbar_from_id": 0, - "busbar_to_id": 1, - "open": false, - "in_service": true, - "asset_bay": null + "asset_bay_id": "VLevel2::L7::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": "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 + "asset_bay_id": "VLevel2::L8::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": "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 - } - ], - "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_connections": [ + "asset_bay_id": "VLevel2::load1::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_id": "L3", - "terminal": null, - "asset_bay_id": "VL3_0::L3::bay" + "asset_bay_id": "VLevel3::L3::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L32_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L32_DISCONNECTOR_5_0", + "BBS3_2": "L32_DISCONNECTOR_5_1" + } }, { - "asset_id": "L6", - "terminal": null, - "asset_bay_id": "VL3_0::L6::bay" + "asset_bay_id": "VLevel3::L6::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L62_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L62_DISCONNECTOR_7_0", + "BBS3_2": "L62_DISCONNECTOR_7_1" + } }, { - "asset_id": "L7", - "terminal": null, - "asset_bay_id": "VL3_0::L7::bay" + "asset_bay_id": "VLevel3::L7::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L72_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L72_DISCONNECTOR_9_0", + "BBS3_2": "L72_DISCONNECTOR_9_1" + } }, { - "asset_id": "L9", - "terminal": null, - "asset_bay_id": "VL3_0::L9::bay" + "asset_bay_id": "VLevel3::L9::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "L91_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "L91_DISCONNECTOR_11_0", + "BBS3_2": "L91_DISCONNECTOR_11_1" + } }, { - "asset_id": "load2", - "terminal": null, - "asset_bay_id": "VL3_0::load2::bay" + "asset_bay_id": "VLevel3::load2::bay", + "sl_switch_grid_model_id": null, + "dv_switch_grid_model_id": "load2_BREAKER", + "sr_switch_grid_model_id": { + "BBS3_1": "load2_DISCONNECTOR_19_0", + "BBS3_2": "load2_DISCONNECTOR_19_1" + } } - ] - } - ], - "assets": [ - { - "grid_model_id": "L1", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "L2", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "L6", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "L7", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "L8", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "load1", - "type": "LOAD", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "L3", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "L9", - "type": "LINE", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - }, - { - "grid_model_id": "load2", - "type": "LOAD", - "name": "", - "in_service": true, - "branch_end": null, - "asset_bay": null, - "asset_bay_id": null - } - ], - "asset_bays": [ - { - "asset_bay_id": "VL2_0::L1::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" - } - }, - { - "asset_bay_id": "VL2_0::L2::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" - } - }, - { - "asset_bay_id": "VL2_0::L6::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" - } - }, - { - "asset_bay_id": "VL2_0::L7::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" - } - }, - { - "asset_bay_id": "VL2_0::L8::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" - } - }, - { - "asset_bay_id": "VL2_0::load1::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_bay_id": "VL3_0::L3::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" - } - }, - { - "asset_bay_id": "VL3_0::L6::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" - } - }, - { - "asset_bay_id": "VL3_0::L7::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" - } - }, - { - "asset_bay_id": "VL3_0::L9::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" - } - }, - { - "asset_bay_id": "VL3_0::load2::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_setpoints": null, - "timestamp": "2025-05-16T15:25:10.814573", - "metrics": null + ], + "asset_setpoints": null, + "timestamp": "2026-06-18T21:43:40.631530", + "metrics": null } 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/original_gridfile/grid.xiidm similarity index 100% rename from packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/test_ucte_powsybl_example.xiidm rename to packages/dc_solver_pkg/tests/files/test_grid_node_breaker/initial_topology/original_gridfile/grid.xiidm 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..c0a0d9f30 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/loadflow_parameters.json @@ -0,0 +1 @@ +{"version": "1.10", "voltageInitMode": "DC_VALUES", "transformerVoltageControlOn": false, "phaseShifterRegulationOn": false, "useReactiveLimits": true, "twtSplitShuntAdmittance": false, "shuntCompensatorVoltageControlOn": false, "readSlackBus": true, "writeSlackBus": true, "dc": false, "distributedSlack": true, "balanceType": "PROPORTIONAL_TO_GENERATION_P", "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": "LEAVE_ON_SLACK_BUS", "voltageRemoteControl": true, "voltageRemoteControlRobustMode": true, "lowImpedanceBranchMode": "REPLACE_BY_ZERO_IMPEDANCE_LINE", "loadPowerFactorConstant": false, "plausibleActivePowerLimit": 10000.0, "newtonRaphsonStoppingCriteriaType": "UNIFORM_CRITERIA", "maxActivePowerMismatch": 0.01, "maxReactivePowerMismatch": 0.01, "maxVoltageMismatch": 0.0001, "maxAngleMismatch": 1e-05, "maxRatioMismatch": 1e-05, "maxSusceptanceMismatch": 0.0001, "slackBusPMaxMismatch": 1.0, "voltagePerReactivePowerControl": false, "generatorReactivePowerRemoteControl": false, "transformerReactivePowerControl": false, "maxNewtonRaphsonIterations": 15, "maxOuterLoopIterations": 20, "newtonRaphsonConvEpsPerEq": 0.0001, "voltageInitModeOverride": "NONE", "transformerVoltageControlMode": "INCREMENTAL_VOLTAGE_CONTROL", "shuntVoltageControlMode": "WITH_GENERATOR_VOLTAGE_CONTROL", "minPlausibleTargetVoltage": 0.8, "maxPlausibleTargetVoltage": 1.2, "minNominalVoltageTargetVoltageCheck": 20.0, "minRealisticVoltage": 0.5, "maxRealisticVoltage": 2.0, "minNominalVoltageRealisticVoltageCheck": 0.0, "lowImpedanceThreshold": 1e-08, "reactiveRangeCheckMode": "MAX", "networkCacheEnabled": false, "svcVoltageMonitoring": true, "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": true, "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": "FIRST_SLACK", "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": {"slackDistributionFailureBehavior": "LEAVE_ON_SLACK_BUS"}} 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 0000000000000000000000000000000000000000..2ca75c0f8ed4583aeacee433d7a9eee12a5ffd95 GIT binary patch literal 150 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1JlVqr_qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= ZXCxM+0{I$7Mmm~03bhL411<)v007R;8>9dL literal 0 HcmV?d00001 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 ffc2654084a81bdf0af4f96ec151aebcc8f49260..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17463 zcmeGkS!^Unb;jrT+AH2G0qsWN!vWSiV|%^c5NUUPXj$ycu6C11`>`tZ%(Ok-+cQ1t zZhLJGDF^~K6eJ=|I07gi5DxJX_(%u|vBMEgi3mdAgX;?=A4vI#lzc>j_g+G6UXxVFeF~=NzR?B*} zGYH++ZRZ-xX`bfup0ub=V(k(C&X4;a^9Oia$;-|9e1CDiP+A(y+KxV0n72Gba~y5S z=e>X}YZqP5na_Fv#oKA1&pV4bG2{_{I`*JH5zqLa_AmJZ)A8xVbmCKfzKgf#yrq(k zKrORl{>rlGZ{_j3{d7A0q5r^-is_i=XI92zEn;R^Ua&v>$H)Hu@@mhOUyUF5;Y+Kl zuiXPJ&jxkXt{~L->be@YS@>R}yS6&^6C%8--;S7`2)eEtLtj~X5q!WpFAz#Uy*blt*XBg&{yLXeO2EoyqN!QCA}9<3w$q0 zK3+U6=KtbpMNf_QK@&hFT~Ntz>+&06+#Hv%zeyx{B1C;!>` z%cYgGHBUtLy7AVT^`1m-PdPbOWT|(6qz(4Nvb7w$t{ZcCFKU(v5Ff~%<^Z#iOSkza z&GA4n$%-jhsYRAEW@ni&SQ{WKHTLyQKZ$IA*0EieW$pQrr8~?r3P$8y<6^D}Trd0y zHE`$MtX}f8Vpd<5(~IZN5M=N?RaLL<59>s%d)BcFq|Wt7fhrqiRyBVh!ltU#B32p} zQs0%{2Tx)q;n@vYVW1Y(2$V5zm1NaZwX3IUmmDT^y)@Y~%qd;dX~iB2d&mLoPwNkG9FhS4yswx4m2` z`oKj5D5J2&r`v>|I7(n^nKc1^By#<|4fP{ zAtt+jW=KAV<#R+nPsryeK6@`sP2M*#HObN!Ca3)TvpVmV3ZgK!%%mV%n;F7l7>f}s zPGB*L#Yrs2e4Y0NGi3_4drmLvjt065knQMA02e(09X&?@a8Jc*30U4u(<*8OSOuI| z*Znj4f^N~|Y%K!(xa?T;SeJIDFqj%hK z6a_hqFFvQn5!HC2tZ`H{4m0YNBgaJJh-$nAai)^0m0)P-5Id>h%NtLrMndFsfFEM1 zl!6znIxkn+KwL3D|0@2|`MwJ2su>1T#&}#vxNYWqddlaUeSpT$QiX_G$M3uB=idze zt?_nfVdEg^GG4>+UFW9KQrr&0cb$x08tv02;7 z=?w&NMaIlxi0Jw0RF*I<0h;2>~EWO-sJIW&??r9i2>ORjfJUqld}ANF+D zV~`br1Aw@2DPu8xQ7b@T0lOC_2T`MY%~i~rZD2bF%yuZv4lFvcNMO;0MK=~bSoC7i z2L*2@3=zAH60m`aW&lgalblXoRy zyYB^O;5kNS-UDkcg5~9T-k*W#&%u<}F|ru%)i}Q0u=<)6nz@Fdsjyk9thREpy~YZv zh*CyfdoVv`h6}=lgg2+>{AIpZ>|otxnh5z(r)%?tc455-l$Gv~Bs&#L@(Hr0A6i|F z?~&#qXF%qQF03wYkWM@JFTtorDYTR+a8`o&61PeOUl?72S4k+o$l4z$H>it^C#Dco zGJX&;^KfN0#6dX%z84M+j0L?6MdZwpILur`3mU)8Em?-gD(sG09)RbEBKlD-xMX}! z(Jn&n#tfUmZiEmhN?Oyc!fPI)MOw?8p=a#MntGD_c7>?wh`m*a{g86zkxu;95Y3!r zXN0bUNOJmO1aAtZ;3O8DU{nOP-ExNRd?o^gT1$mOD1!vC?Cd%mM1mib;2W5Mg0Iko zU7G8{$&zQ6Y|Ea5Q=s?OoHZFmPj?_|)7`=9!HIhnTyV&gV36izJ;vzBsYfA((81?N z>e|u4MZ?Gye7<`Q4R(PH_N)Q+mCqdyt^4tHZToRFdu0Z)qU_1`xq-N<={xg$fD(SG+4VdKUmf}Yv@+a1sGh` zhx`E|V}1u{5ef6#_=b!;%=^p0N{+DrP7Kk_XUxZ;zfySRyblT9&2K5B9QlS;#62A+ zV$1X9ccGRxzXxSQHq7tixOIsRM-qirBHFS=qB>8=Q-`wk>l5oYA&w*JEMpG_g5#H_ z&Q4EVl55!+R8gbMH7NKlvA&GX4aAc6$|lI(u_4ae?2=yeSHu)}m)HjxIOa`93h@_+ zoEG$F_(rsTif=#1f|fRr>2uH&wUB*f+~1LuqJ1Gi_ZxgeoUocmWV(frOBPM!;D&kA zc?Zq3Oz^ueUpKsL9#iDAXzJ>S9M#zFxhBOGZ#r#t_l=*w8 zMX3y?pTd~Dj6jG($(%5luV6DN=U-}3AbV-z4@5Tj`TA z40KfpDwPZ*71^mo71_CivKreiS&h|~6?9d~3J-Y12HZar(HY7rqI1V&C6BqtT;<=c zwN>ej*r`4`E332p4vNl7S%o{WPPdX)o$i}0FP-lf!G&;ic-TPa(GmGi>vRXCcaTu- zi^HV@WYhRh{wNpPCosuVZDYakmKvj>8W z;se-5UhEmHj$oBs+3%nlI6(6YP>XV95cuO+C1K#5BJh{-Wm*t;4ACOdAkHW1SHYt{ zUI!`0^}{Nz4`XBV2}DIfs)*XQIWit*$}I=sxcP&CnA1Bx43}X0z zvxpczQ_b5=h~YQJ8(6(6xy=Q<$^4>q`{kyaXbf;^>qoRok8e1l9gc`-&%q=i^oOnr zFM+oq=1jf5)DDIB=9{>IX_;Dd}o*N3LdS*^V1f8@+_G4FtAX0e4DE3HX=XQu9JQ3SO4lJ)tw;`^q zrxg?#R_R45RE#>^+aZufhXT5ZQCRI&PTO?5h!XoLLX4G1iQ@i+Vzb78DkfCnr3Azn zZ`(scxc!t7uh^Gs$?%*J~Q1W>qyvhq-AMq!;J6~(xv-@y&Z{gqmgLseF z2{)jQkp=kbsFkB7C26jr8F>h+pTXisSbZ9bAVTGbT?4Kr4LG#TF~OKRg!jz{0j4S< zAvB*wTtwt^_=c3yMCGbqFXA=7q(+)w#y2!=jU!$28#wM6EPjlI@}+NJr4=W#7mJs$ z`Y$YwK((Hl$Yf#}lL@IK!3$;qb(EwGOzBAQBrr+??gqluOF*W3>xqL#tCSWC_B_rL zSMr7yTHcK^yv&2oZ}4z4>Ta}KZKxsI4I=4+y@fxRI|@(kG3rt+Bux~F#{0ty%XMkR z#pdk>T=IVc$9v>mYluLeZeqrWx7YZsT{QpVGVdZW3kjE-7fI zhYV3$?Zw5iNDt}Mr)C=y3Ocvm*rkd{EX-e{U5Ivz;o%ACG44+Ik?o>yR>gtE9}vOK zGGYJ2+Oe&RQ7;4CjCO4AwHCR`4QdBkUC<6x5TPXG*628^sT~m>ce7jd|6DtiYLKr7 z9cgMu({`w1dErom*%12C)Q+Zh1ll12dU7jG&mvCco7xd-2iOgAE=|vZpE%)%Q%&s% zv;&nxDu#$anx3Vp9ZEYWUqP3G|54G@j;3}f?I5orj0U|nbC#xO2~WewY^aa}eat(R z(drufaT0x$&onrEEtTLP8^1e-^UkaObc+T>3<~^z$!O8T=SCX={b6|y41Wz~WalmV VqXhZK+3A3`{3}{u0Kc*y{4ZR7-k<;g 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..151b2e17a 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,205 @@ -{ - "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_2 ## VL2_3 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L2", + "name": "VL1_4 ## VL2_5 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "L3", + "name": "VL1_6 ## VL3_4 ## ", + "type": "LINE", + "kind": "branch" + }, + { + "id": "BBS2_1", + "name": "bus1", + "type": "busbar", + "kind": "bus" + }, + { + "id": "BBS2_2", + "name": "bus2", + "type": "busbar", + "kind": "bus" + }, + { + "id": "BBS2_3", + "name": "bus3", + "type": "busbar", + "kind": "bus" + }, + { + "id": "VL2_BREAKER", + "name": "VL2_BREAKER", + "type": "BREAKER", + "kind": "switch" + }, + { + "id": "VL2_BREAKER#0", + "name": "VL2_BREAKER#0", + "type": "BREAKER", + "kind": "switch" + } + ], + "contingencies": [ + { + "elements": [], + "id": "BASECASE", + "name": "" + }, + { + "elements": [ + { + "id": "L1", + "name": "VL1_2 ## VL2_3 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L1", + "name": "VL1_2 ## VL2_3 ## " + }, + { + "elements": [ + { + "id": "L2", + "name": "VL1_4 ## VL2_5 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L2", + "name": "VL1_4 ## VL2_5 ## " + }, + { + "elements": [ + { + "id": "L3", + "name": "VL1_6 ## VL3_4 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L3", + "name": "VL1_6 ## VL3_4 ## " + }, + { + "elements": [ + { + "id": "L4", + "name": "VL1_8 ## VL4_4 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L4", + "name": "VL1_8 ## VL4_4 ## " + }, + { + "elements": [ + { + "id": "L5", + "name": "VL1_10 ## VL4_6 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L5", + "name": "VL1_10 ## VL4_6 ## " + }, + { + "elements": [ + { + "id": "L6", + "name": "VL2_7 ## VL3_6 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L6", + "name": "VL2_7 ## VL3_6 ## " + }, + { + "elements": [ + { + "id": "L7", + "name": "VL2_9 ## VL3_8 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L7", + "name": "VL2_9 ## VL3_8 ## " + }, + { + "elements": [ + { + "id": "L8", + "name": "VL2_11 ## VL4_8 ## ", + "type": "LINE", + "kind": "branch" + } + ], + "id": "L8", + "name": "VL2_11 ## VL4_8 ## " + }, + { + "elements": [ + { + "id": "generator3", + "name": "", + "type": "GENERATOR", + "kind": "injection" + } + ], + "id": "generator3", + "name": "" + }, + { + "elements": [ + { + "id": "load2", + "name": "", + "type": "LOAD", + "kind": "injection" + } + ], + "id": "load2", + "name": "" + }, + { + "elements": [ + { + "id": "load6", + "name": "", + "type": "LOAD", + "kind": "injection" + } + ], + "id": "load6", + "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/optimizer_snapshot/run_0/res.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/res.json new file mode 100644 index 000000000..ed7488101 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/res.json @@ -0,0 +1,98 @@ +{ + "total_branch_combis": 2532808, + "total_inj_combis": 2532808, + "total_num_splits": 0, + "max_fitness": -88.39493471678644, + "best_topos": [ + { + "actions": [ + 5 + ], + "disconnections": [], + "pst_setpoints": null, + "metrics": { + "fitness": -88.39493471678644, + "extra_scores": { + "overload_energy_n_1": 88.39493471678644, + "split_subs": 1.0, + "top_k_overloads_n_1": 133.17284815697712 + }, + "worst_k_contingency_cases": [ + "generator3", + "L8" + ] + }, + "loadflow_results": null + } + ], + "initial_fitness": -138.25617430975592, + "initial_metrics": { + "overload_energy_n_1": 138.25617430975592, + "split_subs": 0.0 + }, + "args": { + "ga_config": { + "runtime_seconds": 30.0, + "iterations_per_epoch": 100, + "random_seed": 42, + "enable_nodal_inj_optim": false, + "plot": false, + "n_worst_contingencies": 2, + "enable_bb_outage": false, + "bb_outage_as_nminus1": true, + "clip_bb_outage_penalty": false, + "bb_outage_more_islands_penalty": 0.0, + "mutation_repetition": 1, + "random_topo_prob": 0.1, + "n_subs_mutated_lambda": 2.0, + "add_split_prob": 0.25, + "change_split_prob": 0.3, + "remove_split_prob": 0.25, + "add_disconnection_prob": 0.2, + "change_disconnection_prob": 0.2, + "remove_disconnection_prob": 0.2, + "pst_mutation_sigma": 0.0, + "pst_mutation_probability": 0.2, + "pst_reset_probability": 0.0, + "proportion_crossover": 0.1, + "crossover_mutation_ratio": 0.5, + "target_metrics": [ + [ + "overload_energy_n_1", + 1.0 + ] + ], + "observed_metrics": [ + "overload_energy_n_1", + "split_subs" + ], + "me_descriptors": [ + { + "metric": "split_subs", + "num_cells": 4, + "range": null + } + ], + "cell_depth": 1 + }, + "lf_config": { + "max_num_splits": 4, + "max_num_disconnections": 0, + "batch_size": 8, + "distributed": false, + "cross_coupler_flow": false + }, + "fixed_files": [ + "../data/test_grid_node_breaker/grid/static_information.hdf5" + ], + "tensorboard_dir": "../data/test_grid_node_breaker/results/test", + "stats_dir": "../data/test_grid_node_breaker/results/test", + "summary_frequency": 10, + "checkpoint_frequency": 100, + "double_limits": [ + 1.0, + 1.0 + ] + }, + "iteration": 3166 +} diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_loadflow_results.csv b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_loadflow_results.csv new file mode 100644 index 000000000..8dbdf83a7 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_loadflow_results.csv @@ -0,0 +1,73 @@ +timestep,contingency,element,side,p,q,i,loading,element_name,contingency_name +0,BASECASE,L1,1,23.397332812941563,2.5096278735439626,60.38196197681353,0.6709106886312615,VL1_2 ## VL2_3 ## ,"" +0,BASECASE,L2,1,23.397332812941563,2.5096278735439626,60.38196197681353,0.6709106886312615,VL1_4 ## VL2_5 ## ,"" +0,BASECASE,L3,1,35.156138126733296,2.7085548884780764,90.4780288022079,1.0053114311356433,VL1_6 ## VL3_4 ## ,"" +0,L1,L2,1,39.270438968568286,4.051466896314793,101.30284664189512,1.1255871849099457,VL1_4 ## VL2_5 ## ,VL1_2 ## VL2_3 ## +0,L1,L3,1,39.33816630600753,2.8857047097261557,101.21300913443238,1.124588990382582,VL1_6 ## VL3_4 ## ,VL1_2 ## VL2_3 ## +0,load6,L1,1,12.74054859893965,2.593044329123728,33.36250155868797,0.37069446176319965,VL1_2 ## VL2_3 ## ,"" +0,load6,L2,1,12.74054859893965,2.593044329123728,33.36250155868797,0.37069446176319965,VL1_4 ## VL2_5 ## ,"" +0,load6,L3,1,19.198623809885508,3.3118523010731593,49.991309325882405,0.5554589925098045,VL1_6 ## VL3_4 ## ,"" +0,L2,L1,1,39.270438968568286,4.051466896314793,101.30284664189512,1.1255871849099457,VL1_2 ## VL2_3 ## ,VL1_4 ## VL2_5 ## +0,L2,L3,1,39.33816630600753,2.8857047097261557,101.21300913443238,1.124588990382582,VL1_6 ## VL3_4 ## ,VL1_4 ## VL2_5 ## +0,L3,L1,1,34.38677379330327,3.0225805794927396,88.5767187043908,0.98418576338212,VL1_2 ## VL2_3 ## ,VL1_6 ## VL3_4 ## +0,L3,L2,1,34.38677379330327,3.0225805794927396,88.5767187043908,0.98418576338212,VL1_4 ## VL2_5 ## ,VL1_6 ## VL3_4 ## +0,L4,L1,1,25.920807689745196,2.7328503720490005,66.88146839335323,0.7431274265928137,VL1_2 ## VL2_3 ## ,VL1_8 ## VL4_4 ## +0,L4,L2,1,25.920807689745196,2.7328503720490005,66.88146839335323,0.7431274265928137,VL1_4 ## VL2_5 ## ,VL1_8 ## VL4_4 ## +0,L4,L3,1,38.94570674216824,2.929614454693308,100.21707187226625,1.1135230208029583,VL1_6 ## VL3_4 ## ,VL1_8 ## VL4_4 ## +0,generator3,L1,1,55.32925526004382,8.77265739544697,143.74843149839376,1.5972047944265972,VL1_2 ## VL2_3 ## ,"" +0,generator3,L2,1,55.32925526004382,8.77265739544697,143.74843149839376,1.5972047944265972,VL1_4 ## VL2_5 ## ,"" +0,generator3,L3,1,83.26332957256895,10.655787910880576,215.39631945855612,2.3932924384284013,VL1_6 ## VL3_4 ## ,"" +0,L5,L1,1,25.920807689745196,2.7328503720490005,66.88146839335323,0.7431274265928137,VL1_2 ## VL2_3 ## ,VL1_10 ## VL4_6 ## +0,L5,L2,1,25.920807689745196,2.7328503720490005,66.88146839335323,0.7431274265928137,VL1_4 ## VL2_5 ## ,VL1_10 ## VL4_6 ## +0,L5,L3,1,38.94570674216824,2.929614454693308,100.21707187226625,1.1135230208029583,VL1_6 ## VL3_4 ## ,VL1_10 ## VL4_6 ## +0,load2,L1,1,12.761821550167141,0.9025890201542097,32.828649158137345,0.3647627684237483,VL1_2 ## VL2_3 ## ,"" +0,load2,L2,1,12.761821550167141,0.9025890201542097,32.828649158137345,0.3647627684237483,VL1_4 ## VL2_5 ## ,"" +0,load2,L3,1,19.15460435308123,0.7790085238216449,49.19136879045235,0.5465707643383595,VL1_6 ## VL3_4 ## ,"" +0,L6,L1,1,14.267278361652258,1.7925437303588865,36.897673641556494,0.40997415157284994,VL1_2 ## VL2_3 ## ,VL2_7 ## VL3_6 ## +0,L6,L2,1,14.267278361652258,1.7925437303588865,36.897673641556494,0.40997415157284994,VL1_4 ## VL2_5 ## ,VL2_7 ## VL3_6 ## +0,L6,L3,1,21.44935945481754,2.0443661301587923,55.288511008361574,0.6143167889817953,VL1_6 ## VL3_4 ## ,VL2_7 ## VL3_6 ## +0,L7,L1,1,-1.655942050149406e-7,-6.276081876421813e-7,1.665557117939689e-6,1.850619019932988e-8,VL1_2 ## VL2_3 ## ,VL2_9 ## VL3_8 ## +0,L7,L2,1,-1.655942050149406e-7,-6.276081876421813e-7,1.665557117939689e-6,1.850619019932988e-8,VL1_4 ## VL2_5 ## ,VL2_9 ## VL3_8 ## +0,L7,L3,1,61.1949960491749,3.6764942612595775,157.30956404489243,1.7478840449432491,VL1_6 ## VL3_4 ## ,VL2_9 ## VL3_8 ## +0,L8,L1,1,42.868273813250354,4.329388500988074,110.5595943284707,1.2284399369830077,VL1_2 ## VL2_3 ## ,VL2_11 ## VL4_8 ## +0,L8,L2,1,42.868273813250354,4.329388500988074,110.5595943284707,1.2284399369830077,VL1_4 ## VL2_5 ## ,VL2_11 ## VL4_8 ## +0,L8,L3,1,64.40052895492168,4.560110947236495,165.66559090541378,1.8407287878379308,VL1_6 ## VL3_4 ## ,VL2_11 ## VL4_8 ## +0,load1,L1,1,21.272108118463617,1.4746961934413605,54.71526371499266,0.6079473746110295,VL1_2 ## VL2_3 ## ,"" +0,load1,L2,1,21.272108118463617,1.4746961934413605,54.71526371499266,0.6079473746110295,VL1_4 ## VL2_5 ## ,"" +0,load1,L3,1,31.926614823201483,1.2538768243345544,81.98688808788444,0.910965423198716,VL1_6 ## VL3_4 ## ,"" +0,BASECASE,L1,2,-23.395145224141654,-2.4002484335793044,60.38196197681373,NaN,VL1_2 ## VL2_3 ## ,"" +0,BASECASE,L2,2,-23.395145224141654,-2.4002484335793044,60.38196197681373,NaN,VL1_4 ## VL2_5 ## ,"" +0,BASECASE,L3,2,-35.13157930564553,-2.217378466722371,90.47802880220793,NaN,VL1_6 ## VL3_4 ## ,"" +0,L1,L2,2,-39.26428160852504,-3.743598894182503,101.30284664189493,NaN,VL1_4 ## VL2_5 ## ,VL1_2 ## VL2_3 ## +0,L1,L3,2,-39.307434086353396,-2.2710603166434975,101.21300913443247,NaN,VL1_6 ## VL3_4 ## ,VL1_2 ## VL2_3 ## +0,load6,L1,2,-12.739880765032874,-2.559652633815988,33.36250155868797,NaN,VL1_2 ## VL2_3 ## ,"" +0,load6,L2,2,-12.739880765032874,-2.559652633815988,33.36250155868797,NaN,VL1_4 ## VL2_5 ## ,"" +0,load6,L3,2,-19.19112641686121,-3.1619044405869023,49.99130932588256,NaN,VL1_6 ## VL3_4 ## ,"" +0,L2,L1,2,-39.26428160852504,-3.743598894182503,101.30284664189493,NaN,VL1_2 ## VL2_3 ## ,VL1_4 ## VL2_5 ## +0,L2,L3,2,-39.307434086353396,-2.2710603166434975,101.21300913443247,NaN,VL1_6 ## VL3_4 ## ,VL1_4 ## VL2_5 ## +0,L3,L1,2,-34.38206629224479,-2.7872055265999354,88.57671870439097,NaN,VL1_2 ## VL2_3 ## ,VL1_6 ## VL3_4 ## +0,L3,L2,2,-34.38206629224479,-2.7872055265999354,88.57671870439097,NaN,VL1_4 ## VL2_5 ## ,VL1_6 ## VL3_4 ## +0,L4,L1,2,-25.918123811255906,-2.5986564476156038,66.88146839335343,NaN,VL1_2 ## VL2_3 ## ,VL1_8 ## VL4_4 ## +0,L4,L2,2,-25.918123811255906,-2.5986564476156038,66.88146839335343,NaN,VL1_4 ## VL2_5 ## ,VL1_8 ## VL4_4 ## +0,L4,L3,2,-38.91557635768431,-2.3270067650141764,100.2170718722663,NaN,VL1_6 ## VL3_4 ## ,VL1_8 ## VL4_4 ## +0,generator3,L1,2,-55.316857093108275,-8.15274904869914,143.74843149839347,NaN,VL1_2 ## VL2_3 ## ,"" +0,generator3,L2,2,-55.316857093108275,-8.15274904869914,143.74843149839347,NaN,VL1_4 ## VL2_5 ## ,"" +0,generator3,L3,2,-83.12414284926014,-7.872053444702995,215.39631945855612,NaN,VL1_6 ## VL3_4 ## ,"" +0,L5,L1,2,-25.918123811255906,-2.5986564476156038,66.88146839335343,NaN,VL1_2 ## VL2_3 ## ,VL1_10 ## VL4_6 ## +0,L5,L2,2,-25.918123811255906,-2.5986564476156038,66.88146839335343,NaN,VL1_4 ## VL2_5 ## ,VL1_10 ## VL4_6 ## +0,L5,L3,2,-38.91557635768431,-2.3270067650141764,100.2170718722663,NaN,VL1_6 ## VL3_4 ## ,VL1_10 ## VL4_6 ## +0,load2,L1,2,-12.761174918043187,-0.87025741398682,32.828649158137345,NaN,VL1_2 ## VL2_3 ## ,"" +0,load2,L2,2,-12.761174918043187,-0.87025741398682,32.828649158137345,NaN,VL1_4 ## VL2_5 ## ,"" +0,load2,L3,2,-19.147344980790834,-0.6338210780130232,49.19136879045241,NaN,VL1_6 ## VL3_4 ## ,"" +0,L6,L1,2,-14.266461498659503,-1.7517005807534087,36.897673641556494,NaN,VL1_2 ## VL2_3 ## ,VL2_7 ## VL3_6 ## +0,L6,L2,2,-14.266461498659503,-1.7517005807534087,36.897673641556494,NaN,VL1_4 ## VL2_5 ## ,VL2_7 ## VL3_6 ## +0,L6,L3,2,-21.440188996469114,-1.8609569631878848,55.28851100836166,NaN,VL1_6 ## VL3_4 ## ,VL2_7 ## VL3_6 ## +0,L7,L1,2,1.65594205035578e-7,6.276088982631336e-7,1.6655571324755098e-6,NaN,VL1_2 ## VL2_3 ## ,VL2_9 ## VL3_8 ## +0,L7,L2,2,1.65594205035578e-7,6.276088982631336e-7,1.6655571324755098e-6,NaN,VL1_4 ## VL2_5 ## ,VL2_9 ## VL3_8 ## +0,L7,L3,2,-61.12075715235486,-2.1917163248596228,157.30956404489243,NaN,VL1_6 ## VL3_4 ## ,VL2_9 ## VL3_8 ## +0,L8,L1,2,-42.86093975891092,-3.9626857840457643,110.5595943284707,NaN,VL1_2 ## VL2_3 ## ,VL2_11 ## VL4_8 ## +0,L8,L2,2,-42.86093975891092,-3.9626857840457643,110.5595943284707,NaN,VL1_4 ## VL2_5 ## ,VL2_11 ## VL4_8 ## +0,L8,L3,2,-64.31819369089166,-2.913405666634582,165.66559090541375,NaN,VL1_6 ## VL3_4 ## ,VL2_11 ## VL4_8 ## +0,load1,L1,2,-21.270311862413,-1.38488339093929,54.71526371499254,NaN,VL1_2 ## VL2_3 ## ,"" +0,load1,L2,2,-21.270311862413,-1.38488339093929,54.71526371499254,NaN,VL1_4 ## VL2_5 ## ,"" +0,load1,L3,2,-31.906449273746478,-0.8505658352350752,81.98688808788441,NaN,VL1_6 ## VL3_4 ## ,"" diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_metrics.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_metrics.json new file mode 100644 index 000000000..a5edca999 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/ac_metrics.json @@ -0,0 +1,40 @@ +{ + "fitness": 89.84894757967612, + "extra_scores": { + "max_flow_n_1": 2.3932924384284013, + "overload_energy_n_1": 89.84894757967612, + "max_va_diff_n_1": 1.3844259249571096, + "overload_current_n_1": 232.89318245534363, + "critical_branch_count_n_1": 3.0, + "max_flow_n_0": 1.0053114311356433, + "overload_energy_n_0": 0.1857428463181492, + "max_va_diff_n_0": -0.8918558568711066, + "overload_current_n_0": 0.4780288022078878, + "critical_branch_count_n_0": 1.0, + "split_subs": 1.0, + "disconnected_branches": 0.0, + "non_converging_loadflows": 0.0, + "switching_distance": 0.0 + }, + "worst_k_contingency_cases": null, + "dc_info": { + "actions": [ + 5 + ], + "disconnections": [], + "pst_setpoints": null, + "metrics": { + "fitness": -88.39493471678644, + "extra_scores": { + "overload_energy_n_1": 88.39493471678644, + "split_subs": 1.0, + "top_k_overloads_n_1": 133.17284815697712 + }, + "worst_k_contingency_cases": [ + "generator3", + "L8" + ] + }, + "loadflow_results": null + } +} diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/dc_loadflow_results.csv b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/dc_loadflow_results.csv new file mode 100644 index 000000000..85ca9a01b --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/dc_loadflow_results.csv @@ -0,0 +1,73 @@ +timestep,contingency,element,side,p,q,i,loading,element_name,contingency_name +0,BASECASE,L1,1,23.404255319148934,NaN,60.0553471497483,0.6672816349972034,VL1_2 ## VL2_3 ## ,"" +0,BASECASE,L2,1,23.404255319148934,NaN,60.0553471497483,0.6672816349972034,VL1_4 ## VL2_5 ## ,"" +0,BASECASE,L3,1,35.1063829787234,NaN,90.08302072462246,1.0009224524958051,VL1_6 ## VL3_4 ## ,"" +0,L1,L2,1,39.285714285714285,NaN,100.80718985850609,1.1200798873167344,VL1_4 ## VL2_5 ## ,VL1_2 ## VL2_3 ## +0,L1,L3,1,39.285714285714285,NaN,100.80718985850609,1.1200798873167344,VL1_6 ## VL3_4 ## ,VL1_2 ## VL2_3 ## +0,load6,L1,1,2.127659574468084,NaN,5.45957701361348,0.06066196681792755,VL1_2 ## VL2_3 ## ,"" +0,load6,L2,1,2.127659574468084,NaN,5.45957701361348,0.06066196681792755,VL1_4 ## VL2_5 ## ,"" +0,load6,L3,1,3.191489361702126,NaN,8.18936552042022,0.09099295022689133,VL1_6 ## VL3_4 ## ,"" +0,L2,L1,1,39.285714285714285,NaN,100.80718985850609,1.1200798873167344,VL1_2 ## VL2_3 ## ,VL1_4 ## VL2_5 ## +0,L2,L3,1,39.285714285714285,NaN,100.80718985850609,1.1200798873167344,VL1_6 ## VL3_4 ## ,VL1_4 ## VL2_5 ## +0,L3,L1,1,34.374999999999986,NaN,88.2062911261928,0.9800699014021421,VL1_2 ## VL2_3 ## ,VL1_6 ## VL3_4 ## +0,L3,L2,1,34.374999999999986,NaN,88.2062911261928,0.9800699014021421,VL1_4 ## VL2_5 ## ,VL1_6 ## VL3_4 ## +0,L4,L1,1,25.925925925925924,NaN,66.52595694366057,0.7391772993740063,VL1_2 ## VL2_3 ## ,VL1_8 ## VL4_4 ## +0,L4,L2,1,25.925925925925924,NaN,66.52595694366057,0.7391772993740063,VL1_4 ## VL2_5 ## ,VL1_8 ## VL4_4 ## +0,L4,L3,1,38.888888888888886,NaN,99.78893541549087,1.1087659490610096,VL1_6 ## VL3_4 ## ,VL1_8 ## VL4_4 ## +0,generator3,L1,1,55.3191489361702,NaN,141.94900235395053,1.577211137266117,VL1_2 ## VL2_3 ## ,"" +0,generator3,L2,1,55.3191489361702,NaN,141.94900235395053,1.577211137266117,VL1_4 ## VL2_5 ## ,"" +0,generator3,L3,1,82.97872340425532,NaN,212.9235035309258,2.3658167058991757,VL1_6 ## VL3_4 ## ,"" +0,L5,L1,1,25.925925925925924,NaN,66.52595694366057,0.7391772993740063,VL1_2 ## VL2_3 ## ,VL1_10 ## VL4_6 ## +0,L5,L2,1,25.925925925925924,NaN,66.52595694366057,0.7391772993740063,VL1_4 ## VL2_5 ## ,VL1_10 ## VL4_6 ## +0,L5,L3,1,38.888888888888886,NaN,99.78893541549087,1.1087659490610096,VL1_6 ## VL3_4 ## ,VL1_10 ## VL4_6 ## +0,load2,L1,1,2.127659574468084,NaN,5.45957701361348,0.06066196681792755,VL1_2 ## VL2_3 ## ,"" +0,load2,L2,1,2.127659574468084,NaN,5.45957701361348,0.06066196681792755,VL1_4 ## VL2_5 ## ,"" +0,load2,L3,1,3.191489361702126,NaN,8.18936552042022,0.09099295022689133,VL1_6 ## VL3_4 ## ,"" +0,L6,L1,1,14.285714285714288,NaN,36.65715994854767,0.40730177720608524,VL1_2 ## VL2_3 ## ,VL2_7 ## VL3_6 ## +0,L6,L2,1,14.285714285714288,NaN,36.65715994854767,0.40730177720608524,VL1_4 ## VL2_5 ## ,VL2_7 ## VL3_6 ## +0,L6,L3,1,21.428571428571434,NaN,54.98573992282152,0.610952665809128,VL1_6 ## VL3_4 ## ,VL2_7 ## VL3_6 ## +0,L7,L1,1,-0.0,NaN,0.0,0.0,VL1_2 ## VL2_3 ## ,VL2_9 ## VL3_8 ## +0,L7,L2,1,-0.0,NaN,0.0,0.0,VL1_4 ## VL2_5 ## ,VL2_9 ## VL3_8 ## +0,L7,L3,1,61.111111111111114,NaN,156.8111842243428,1.7423464913815867,VL1_6 ## VL3_4 ## ,VL2_9 ## VL3_8 ## +0,L8,L1,1,42.85714285714285,NaN,109.97147984564297,1.2219053316182553,VL1_2 ## VL2_3 ## ,VL2_11 ## VL4_8 ## +0,L8,L2,1,42.85714285714285,NaN,109.97147984564297,1.2219053316182553,VL1_4 ## VL2_5 ## ,VL2_11 ## VL4_8 ## +0,L8,L3,1,64.28571428571428,NaN,164.95721976846448,1.832857997427383,VL1_6 ## VL3_4 ## ,VL2_11 ## VL4_8 ## +0,load1,L1,1,10.638297872340424,NaN,27.29788506806741,0.30330983408963785,VL1_2 ## VL2_3 ## ,"" +0,load1,L2,1,10.638297872340424,NaN,27.29788506806741,0.30330983408963785,VL1_4 ## VL2_5 ## ,"" +0,load1,L3,1,15.957446808510637,NaN,40.946827602101116,0.45496475113445684,VL1_6 ## VL3_4 ## ,"" +0,BASECASE,L1,2,-23.404255319148934,NaN,60.0553471497483,NaN,VL1_2 ## VL2_3 ## ,"" +0,BASECASE,L2,2,-23.404255319148934,NaN,60.0553471497483,NaN,VL1_4 ## VL2_5 ## ,"" +0,BASECASE,L3,2,-35.1063829787234,NaN,90.08302072462246,NaN,VL1_6 ## VL3_4 ## ,"" +0,L1,L2,2,-39.285714285714285,NaN,100.80718985850609,NaN,VL1_4 ## VL2_5 ## ,VL1_2 ## VL2_3 ## +0,L1,L3,2,-39.285714285714285,NaN,100.80718985850609,NaN,VL1_6 ## VL3_4 ## ,VL1_2 ## VL2_3 ## +0,load6,L1,2,-2.127659574468084,NaN,5.45957701361348,NaN,VL1_2 ## VL2_3 ## ,"" +0,load6,L2,2,-2.127659574468084,NaN,5.45957701361348,NaN,VL1_4 ## VL2_5 ## ,"" +0,load6,L3,2,-3.191489361702126,NaN,8.18936552042022,NaN,VL1_6 ## VL3_4 ## ,"" +0,L2,L1,2,-39.285714285714285,NaN,100.80718985850609,NaN,VL1_2 ## VL2_3 ## ,VL1_4 ## VL2_5 ## +0,L2,L3,2,-39.285714285714285,NaN,100.80718985850609,NaN,VL1_6 ## VL3_4 ## ,VL1_4 ## VL2_5 ## +0,L3,L1,2,-34.374999999999986,NaN,88.2062911261928,NaN,VL1_2 ## VL2_3 ## ,VL1_6 ## VL3_4 ## +0,L3,L2,2,-34.374999999999986,NaN,88.2062911261928,NaN,VL1_4 ## VL2_5 ## ,VL1_6 ## VL3_4 ## +0,L4,L1,2,-25.925925925925924,NaN,66.52595694366057,NaN,VL1_2 ## VL2_3 ## ,VL1_8 ## VL4_4 ## +0,L4,L2,2,-25.925925925925924,NaN,66.52595694366057,NaN,VL1_4 ## VL2_5 ## ,VL1_8 ## VL4_4 ## +0,L4,L3,2,-38.888888888888886,NaN,99.78893541549087,NaN,VL1_6 ## VL3_4 ## ,VL1_8 ## VL4_4 ## +0,generator3,L1,2,-55.3191489361702,NaN,141.94900235395053,NaN,VL1_2 ## VL2_3 ## ,"" +0,generator3,L2,2,-55.3191489361702,NaN,141.94900235395053,NaN,VL1_4 ## VL2_5 ## ,"" +0,generator3,L3,2,-82.97872340425532,NaN,212.9235035309258,NaN,VL1_6 ## VL3_4 ## ,"" +0,L5,L1,2,-25.925925925925924,NaN,66.52595694366057,NaN,VL1_2 ## VL2_3 ## ,VL1_10 ## VL4_6 ## +0,L5,L2,2,-25.925925925925924,NaN,66.52595694366057,NaN,VL1_4 ## VL2_5 ## ,VL1_10 ## VL4_6 ## +0,L5,L3,2,-38.888888888888886,NaN,99.78893541549087,NaN,VL1_6 ## VL3_4 ## ,VL1_10 ## VL4_6 ## +0,load2,L1,2,-2.127659574468084,NaN,5.45957701361348,NaN,VL1_2 ## VL2_3 ## ,"" +0,load2,L2,2,-2.127659574468084,NaN,5.45957701361348,NaN,VL1_4 ## VL2_5 ## ,"" +0,load2,L3,2,-3.191489361702126,NaN,8.18936552042022,NaN,VL1_6 ## VL3_4 ## ,"" +0,L6,L1,2,-14.285714285714288,NaN,36.65715994854767,NaN,VL1_2 ## VL2_3 ## ,VL2_7 ## VL3_6 ## +0,L6,L2,2,-14.285714285714288,NaN,36.65715994854767,NaN,VL1_4 ## VL2_5 ## ,VL2_7 ## VL3_6 ## +0,L6,L3,2,-21.428571428571434,NaN,54.98573992282152,NaN,VL1_6 ## VL3_4 ## ,VL2_7 ## VL3_6 ## +0,L7,L1,2,0.0,NaN,0.0,NaN,VL1_2 ## VL2_3 ## ,VL2_9 ## VL3_8 ## +0,L7,L2,2,0.0,NaN,0.0,NaN,VL1_4 ## VL2_5 ## ,VL2_9 ## VL3_8 ## +0,L7,L3,2,-61.111111111111114,NaN,156.8111842243428,NaN,VL1_6 ## VL3_4 ## ,VL2_9 ## VL3_8 ## +0,L8,L1,2,-42.85714285714285,NaN,109.97147984564297,NaN,VL1_2 ## VL2_3 ## ,VL2_11 ## VL4_8 ## +0,L8,L2,2,-42.85714285714285,NaN,109.97147984564297,NaN,VL1_4 ## VL2_5 ## ,VL2_11 ## VL4_8 ## +0,L8,L3,2,-64.28571428571428,NaN,164.95721976846448,NaN,VL1_6 ## VL3_4 ## ,VL2_11 ## VL4_8 ## +0,load1,L1,2,-10.638297872340424,NaN,27.29788506806741,NaN,VL1_2 ## VL2_3 ## ,"" +0,load1,L2,2,-10.638297872340424,NaN,27.29788506806741,NaN,VL1_4 ## VL2_5 ## ,"" +0,load1,L3,2,-15.957446808510637,NaN,40.946827602101116,NaN,VL1_6 ## VL3_4 ## ,"" diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/modified_network.xiidm b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/modified_network.xiidm new file mode 100644 index 000000000..b73091602 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/modified_network.xiidm @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/orao_summary.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/orao_summary.json new file mode 100644 index 000000000..692426e90 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/orao_summary.json @@ -0,0 +1 @@ +{"forced-actions": {"preventive-actions-list": {"version": "1.2", "actions": [{"type": "SWITCH", "id": "Open VL2_BREAKER", "switchId": "VL2_BREAKER", "open": true}, {"type": "SWITCH", "id": "Open L12_DISCONNECTOR_4_0", "switchId": "L12_DISCONNECTOR_4_0", "open": true}, {"type": "SWITCH", "id": "Close L12_DISCONNECTOR_4_2", "switchId": "L12_DISCONNECTOR_4_2", "open": false}]}}} diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/sld/VLevel2_sld.svg b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/sld/VLevel2_sld.svg new file mode 100644 index 000000000..dfc8b6140 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/topology_0/sld/VLevel2_sld.svg @@ -0,0 +1,647 @@ + + + + + + + + + bus1 + + + + bus2 + + + + bus3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -27 + + + + + -2 + + + + + 69 + + + + + + + + + + + + + + + + + L1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -27 + + + + + -2 + + + + + 69 + + + + + + + + + + + + + + + + + L2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -28 + + + + + -4 + + + + + 72 + + + + + + + + + + + + + + + + + L6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 54 + + + + + 5 + + + + + 139 + + + + + + + + + + + + + + + + + L7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -72 + + + + + -6 + + + + + 187 + + + + + + + + + + + + + + + + + L8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100 + + + + + 10 + + + + + 259 + + + + + + + + + + + + + + + + + + + + load1 + + + + + + + + + + + + + + 224.4 kV + -1.2° + 0 MW + 0 MVar + + + + 224.9 kV + -0.3° + 0 MW + 0 MVar + + + + + diff --git a/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/unsplit_ac_metrics.json b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/unsplit_ac_metrics.json new file mode 100644 index 000000000..1b9da01d3 --- /dev/null +++ b/packages/dc_solver_pkg/tests/files/test_grid_node_breaker/optimizer_snapshot/run_0/unsplit_ac_metrics.json @@ -0,0 +1,28 @@ +{ + "fitness": 139.52255150807292, + "extra_scores": { + "max_flow_n_1": 2.442972284125944, + "overload_energy_n_1": 139.52255150807292, + "max_va_diff_n_1": 0.9196808487469375, + "overload_current_n_1": 361.43814087357083, + "critical_branch_count_n_1": 3.0, + "max_flow_n_0": 1.3093068610204894, + "overload_energy_n_0": 21.5672909386536, + "max_va_diff_n_0": 0.0, + "overload_current_n_0": 55.675234983688085, + "critical_branch_count_n_0": 2.0, + "split_subs": 0.0, + "disconnected_branches": 0.0, + "non_converging_loadflows": 0.0 + }, + "worst_k_contingency_cases": null, + "dc_info": { + "actions": [], + "disconnections": [], + "fitness": -138.25617430975592, + "metrics": { + "overload_energy_n_1": 138.25617430975592, + "split_subs": 0.0 + } + } +} 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 df692622909982b3f7caa16c2868f465e5a85b9b..5557c66bc81fdec093b36c5755a3bf92eedf4ff7 100644 GIT binary patch delta 1908 zcmb7ET}+!*7(VB;rRAfHf~MdUzOPcC6#9o&FzWaj{~J=77kI(CQa+#z+lCeelL;{~ zaf`+^ukJ#e!;AfxV8Zrk;th&7>io$dM57m|H@jG(2}{%%ld0#N?|faic;ji>=RD7Q z&U@bXocHZl=kU&XeA+{HMsnUZd~Yt^gsr*PUEVE%_^5qK$B%SCC;RltCBc`=b6?h1 z1hG!vZ`9F2G>p$+xJ0VRz#+i+sHT{l*lt083cq7@RIQPMf>w8P8WA+w!|Al3i@ls0 zF%7vXeMeiibl`IPGhcK$udKIExJu~kyH8ZuE|Oh_DuP4?zhf1)YawfE5&m9FlvbJ{ zXfC4hEr?8p8u*b8;bFnbsP;{r4QoGFgo~wqvF=wFUw2y2vmQ>@1ubo26kdBEBJCND zeSF21py6gtgMxmm@D1xW8;n)k$cF)#s!rD1(I}4tqe@hdY$!gPNJq!VqpA69Y)Xx$ z)nsfoy9j?*dSSEF0n6nru%xt+9vM1o{(y5Xh0`|`O5b}7?X~Mk{eOfy`4qYC(k{zI znB01hx^rsR6a|yg2Ds_^8dmzc$W8=)>RY2RC=e;>BF9G{cK;e|xNRge0-w8k?fdEJ zyCFRUxLF~C1N$KDbiiJ*4NlZ4aBIH;tL0r}%tM{7QM2tXa|H}5P8ez!A-{X!-G;ZY z1zz=pNVtaI-L5AhIYi;8ek!fNOO5lSx|uF@YGqyrCOtmQ)#Suv%nz=nR)G9O^s zz%}K?k&kZ-rC!+S-84|;rS2`jpl(0V;Xr!T>~`u&rjhnf zvqZ>l2ucEjOCmIMxK%fsbU=B%O&82Umjq6!Np&ukP0c-|3l^apIGc)1uxl;)550wI zR$X60g&Q8BqD^eb2k@{i6yL3fb-@TIzn(dvqeO?>iHJb$cj!kDI?TeMqg0368$?y? z6FRFDHiG_Ef5@LsWwN0b|AhKnuqiy*qK0EF;@~ov49Iwt6_d6a2=3$6_G~@K?USqy zD~$jQG@&X)`wf0^-&bp)>9gJ=gac(&{7e%bJIE(!6BPGZ{%S_mvbb6D24{W?IlG8L z&Mvr+vx_a{>_Q7Un-_9gAqNm!WE{v!gT11`*}Ram5-8+Ws5aGt+A%|XJ+R$z(nsx> zW{vr$0I0w&gZ>C)i$F0caItfyIMqvSbVzWh>ICgeDc|MW{j|%kU^8rWH%iBt3p$>t z<^6cfY{Ns4Gs`g0N9U$f2rdjSUj6ZC8HTN z8#Sic*jzT6NKPc;YKEq(k7=Ui0!InZKihkRM8Rk~Go10taNg@+-|{0*4tHZ}dxgCV o9Y4UQM3&YCdvuUxDk;MA%Q8>gV;QB1EWq*Mr|rf|mC?Qb0?g!dhX4Qo delta 1236 zcmZ`&T}YEr7(VAX=eM8KRW8YN-<+ZsM%iaj7gASYSdd6v1c6=j0~K`VvQ;V&^Oi%v5+on2FtK(L z+}JD4FK^Ijn|QXHMA@?@%V9O-3?}U`M^|fDSOaT<+IddBcCf&oayVsYP+T}}Vh8RE z0_-mr$Tue(E>FU=unt_6P2@ubj8y!BPhy5Nh*H?B#$>x0-q#J%unIwE86^9wVZ`zR zthgA)3f5`<@P%O5Im%KgDl25#jDF)H?NiSX9JdPOSO891+x7XhKqW4vN?^d9&|@ry z9~uE_*NO$Z{DD@CRMI^|J*Iw3|0&PdZjduI(CaY5jPESCRcgH&p*-c9eSVhbXWp;7 z76yOtPP{B-X}_xx$m?<@=G%kx@;&g|eu9kJrCx^>la~&dsC@=SWwaYkDo)BVYfE@L z-59>RjnX^UPfSW&Fjwz`TBo3K*EQC;shd1Vt~ZhEMRX{;h?Mnj{on67W~G>_f=?~o z7Cq`>F=Dg=S&@JueW`Rh6iJ=vNyPd>-HFsm1jU|Bi|kf1yJ$QeNhOo9NPj$)OotMw zXg9SBfU7k__O!sA){C_m$xX*f=drxAkC9B|3?pdsL^#)Wlw9+`m$vg-dYFIU)*T`f zTOeFE4zu2)#Lx~CRpYe$0^~Yu+Ms-#t^qTv_%v_8P$es~#%t3qyyC2ZQXkorgsMb1 z=JS%?4tVGr1bG}AeH-9hm7D1N%Jo=~c5ExfaH?}2PHmqK&}{qrQi(+PP$CxUN%x21 z$!I(hODo=y*K2G#}1zUOMQH6FLEch6jvP@HJ@CsJNa$p>q{| S7~+4XIlXhYeqn^=5&R1Wa6K9T 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 00359203c58ccdbc361c23f29e40ef87d8fea5a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2196 zcmZ{mc{~%0AIC>?MHc1?iCjfD;&&k(G$;e!$ zmuy%GAjfu!o+`5wMhyH>+Y$clxR7?MHSU#R16q6Jh=Dq7q}6aPxs-#q{?6RAxx%Mg z<0e<1iqQbt16agxA*(2B6w6Ap++?JCR7Vjpo-RNAZck&~3+a11K2fHh$I{EI8>~$&u zldMYl0g_}=mxB&hjyemJLqfOlKnX3cW zFjPWVRunnR9!c>xJy?dfGOo_|T9TICk+eG~=#4<+nS(lblyxTYgO$6>5Okz5+cMV? zcC&pt7LHpLe)$1MahG(@^zzGd{ZUBn_?g3pXiRUQ!-^d~^dl_`((dwyxYCSKxY!5X+z0t_z!w(=iw< z)QF7p0KSP)Z5l5M+T~-7QQlNh5JT&3VwBA@r-i?mpn271v3ol9mQLHm4!-`yfeZ{| zM$H=XE3^FXvrVsUowpM&>T&LLxXk12mE}r3xw4Y+QIyCneDQZ>_Pkghe$R|eO0?q_ zN&PQ04)ic^w`jj~oCzCNrfAkL(p84Q4*ke;xbW;_TdN#M&FR@ddey{bLX0g} zSikJmgbh@R$NfskHuq7PaBr^AH{_*#3rUFQuX7U`hGHuX01{mY?Fqh(ZZ`yh99~51 zlh3hXpoyy}LLS~Bw2g$?XEz-_lD0JvXLa+@-f1pn9xe3zDuiy2wsVt&7T~=3BHu!3 z1T&)%@-Kc4eW1?7@@Ng$nzwy~s%~NQ*u!!iI~MGj_%waAU&dyzx=ku+s0^%K77r^RgXkc+9=}=pvCu1S zFN-)CEe5!H<)tdyyo%p!k8AJB3Dvxvst2vrRY(RPRlg=xvUxz|xl|3(l-xQeAVf+= z4YoELXCb&_fryjU z5<3OtJbS??7Qn*#|1iZ-FEIb`ae1W?u~s|Wg`maX7!q(#352^QcpT8H3u>IL)~>H= zgY-?sKrSLo)JhAaVAWmRTPZcqK25uZOO?u{(_rW-Mw-^*uQtxY`k%M@mV0aa3A)_Q zzhX=W)%cv@v%98_Zka_a(Z=;d6DA~8);O#6Lx)*|F_rHw?@On!X)i}w-)jW zgE8K}i=*Mp_brZe{7;@u!j?tL_kp(P1L3S*o&BwR2_`nmWs}}pQ{v(kh;GAN^>>s3 zWR>Ury)_nfud7!sKY4G~k5US{YBs!MxzvaYhu?Um{wA`-kUHU0luc2X1hz}-XXK@- zu{XbQfkI164pV#1o9)Kv8XuMohPU00wRyabNL5^@?HLN+5S(mV062G66`!vd`dE!f zFL1!b19>BMNzV#!gY1)j(6fwJ@?IIH9+?i7PHMT={8^~ir1uzhJT~~-O2oDBz+jV< zxvoEQkKC^3l$X7dvoLXTdOfY@9 Non framework="pandapower", ) - n_loadflows, n_success, time = run_benchmark(runner, topologies, method=cast(LoadflowType, method)) + n_loadflows, n_success, time = run_benchmark(runner, topologies, method=method) nminus1_definition = load_nminus1_definition( preprocessed_data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"] 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 f09e346e7..59eec9508 100644 --- a/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py +++ b/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py @@ -51,7 +51,6 @@ extract_busbar_outage_data, get_busbar_branches_map, get_busbar_index, - get_connected_assets, get_rel_non_rel_sub_bb_maps, get_total_injection_along_stub_branch, logger, @@ -105,7 +104,7 @@ def test_perform_outage_single_busbar( for station in network_data_preprocessed.asset_topology.materialize_stations(): if station_id == station.grid_model_id: for index, busbar in enumerate(station.busbars): - connected_assets = get_connected_assets(station, index) + connected_assets = station.get_connected_assets(index) connected_branches = [asset.grid_model_id for asset in connected_assets if asset.is_branch()] connected_branches = [ network_data_preprocessed.branch_ids.index(branch_id) for branch_id in connected_branches @@ -201,7 +200,7 @@ def test_perform_outage_single_busbar_with_disconnections( for station in network_data_preprocessed.asset_topology.materialize_stations(): if station_id == station.grid_model_id: for index, busbar in enumerate(station.busbars): - connected_assets = get_connected_assets(station, index) + connected_assets = station.get_connected_assets(index) connected_branches = [asset.grid_model_id for asset in connected_assets if asset.is_branch()] connected_branches = [ network_data_preprocessed.branch_ids.index(branch_id) for branch_id in connected_branches @@ -329,7 +328,10 @@ def test_perform_rel_bb_outage_single_topo_with_no_inj_reassignments( connected_branches_flow = rel_busbar_branches_flows[outage_index] assert jnp.allclose(abs(connected_branches_flow.sum(axis=1)), abs(power_at_node), atol=1e-03), ( - "Kirchoff's current law should be satisfied" + "Kirchoff's current law should be satisfied: " + f"outage_index={outage_index}, nodal_index_bb={nodal_index_bb}, " + f"sumflow={abs(connected_branches_flow.sum(axis=1))}, power_at_node={abs(power_at_node)}, " + f"connected_branches_flow={connected_branches_flow}" ) validate_zero_flows(lfs, success, busbar_data["busbar_br_outage_map"]) @@ -339,8 +341,10 @@ def get_busbar_injection_map( station: MaterializedStation, network: NetworkData ) -> dict[str, Float[np.ndarray, " n_timestep"]]: busbar_injection_map = {} + topology = network.simplified_asset_topology or network.asset_topology + topology_assets = [*topology.branch_assets, *topology.injection_assets] for i, bb in enumerate(station.busbars): - connected_assets = get_connected_assets(station, i) + connected_assets = station.get_connected_assets(i, topology_assets=topology_assets) injection: Float[np.ndarray, " n_timestep"] = np.zeros(network.nodal_injection.shape[0], float) for asset in connected_assets: @@ -457,8 +461,9 @@ def correct_power_flow_directions(lfs, network_data_preprocessed: NetworkData): branches_at_nodes = network_data_preprocessed.branches_at_nodes lfs_corrrected_directions = [] for lfs_instance in lfs: + power_flow_line = lfs_instance for local_branches, local_branches_dir in zip(branches_at_nodes, branch_dir): - power_flow_line = lfs_instance.at[:, local_branches].multiply(np.where(local_branches_dir == 1, -1, 1)) + power_flow_line = power_flow_line.at[:, local_branches].multiply(np.where(local_branches_dir == 1, -1, 1)) lfs_corrrected_directions.append(power_flow_line) return lfs_corrrected_directions @@ -500,7 +505,7 @@ def test_compare_loadflows_non_rel_bb_outage_powsybl( if bb.grid_model_id not in non_rel_bb_outage_map[station.grid_model_id]: continue bb_index = get_busbar_index(station, bb.grid_model_id) - connected_assets = get_connected_assets(station, bb_index) + connected_assets = station.get_connected_assets(bb_index) connected_asset_ids = [asset.grid_model_id for asset in connected_assets] connected_branch_indices = [ network_data.branch_ids.index(asset.grid_model_id) for asset in connected_assets if asset.is_branch() @@ -590,7 +595,7 @@ def test_compare_loadflows_rel_bb_outage( continue copy_net = copy.deepcopy(net) bb_index = get_busbar_index(modified_station, bb.grid_model_id) - connected_assets = get_connected_assets(modified_station, bb_index) + connected_assets = modified_station.get_connected_assets(bb_index) connected_asset_ids = [asset.grid_model_id for asset in connected_assets] connected_branch_indices = [ network_data_test_grid.branch_ids.index(asset.grid_model_id) diff --git a/packages/dc_solver_pkg/tests/numpy_reference.py b/packages/dc_solver_pkg/tests/numpy_reference.py index 595a191ea..c2d496266 100644 --- a/packages/dc_solver_pkg/tests/numpy_reference.py +++ b/packages/dc_solver_pkg/tests/numpy_reference.py @@ -17,6 +17,7 @@ import numpy as np from jaxtyping import Bool, Float, Int +from toop_engine_dc_solver.preprocess.helpers.ptdf import compute_ptdf def _validate_assignment(assignment: Bool[np.ndarray, " n_branches_at_node"]) -> None: @@ -511,6 +512,207 @@ def calc_bsdf( return bsdf, ptdf_new, from_node_new, to_node_new +def calc_bsdf_exact_rebuild( + switched_node: int, + assignment: Bool[np.ndarray, " n_branches_at_node"], + is_slack: bool, + bus_a_ptdf: int, + bus_b_ptdf: int, + ptdf: Float[np.ndarray, " n_branches n_nodes"], + susceptance: Float[np.ndarray, " n_branches"], + from_node: Int[np.ndarray, " n_branches"], + to_node: Int[np.ndarray, " n_branches"], + branches_at_nodes: list[Int[np.ndarray, " n_branches_at_node"]], + branch_direction: list[Bool[np.ndarray, " n_branches_at_node"]], +) -> tuple[ + Float[np.ndarray, " n_branches"], + Float[np.ndarray, " n_branches n_nodes"], + Int[np.ndarray, " n_branches"], + Int[np.ndarray, " n_branches"], + float, +]: + """Prototype an exact split update by rebuilding the PTDF on active connected nodes. + + This is intended as a diagnostic/reference path for cases where the closed-form BSDF update + appears to be structurally wrong. It performs the topology split exactly on the branch endpoint + arrays, recomputes the PTDF on the active connected node set, restores untouched extended + columns, and then projects the exact PTDF delta back onto the standard BSDF rank-1 form. + + Parameters + ---------- + switched_node : int + Index of the node being split. + assignment : Bool[np.ndarray, " n_branches_at_node"] + Boolean mask for branch assignment to busbars. + is_slack : bool + True if the node is the slack node. + bus_a_ptdf : int + PTDF column index for busbar A. + bus_b_ptdf : int + PTDF column index for busbar B. + ptdf : Float[np.ndarray, " n_branches n_nodes"] + PTDF matrix before the split. + susceptance : Float[np.ndarray, " n_branches"] + Branch susceptances. + from_node : Int[np.ndarray, " n_branches"] + Branch from-nodes. + to_node : Int[np.ndarray, " n_branches"] + Branch to-nodes. + branches_at_nodes : list[Int[np.ndarray, " n_branches_at_node"]] + Branch indices at each node. + branch_direction : list[Bool[np.ndarray, " n_branches_at_node"]] + Direction mask for branches at each node. + + Returns + ------- + Float[np.ndarray, " n_branches"] + Least-squares BSDF vector implied by the exact PTDF rebuild. + Float[np.ndarray, " n_branches n_nodes"] + Exact PTDF after the split on the active connected node set. + Int[np.ndarray, " n_branches"] + Updated from-nodes. + Int[np.ndarray, " n_branches"] + Updated to-nodes. + float + Maximum absolute reconstruction error of the projected rank-1 BSDF update. + """ + _validate_assignment(assignment) + + branches_local, direction_local = _get_local_branch_data( + switched_node=switched_node, + branches_at_nodes=branches_at_nodes, + branch_direction=branch_direction, + ) + branch_from_a, branch_to_a, branch_from_b, branch_to_b, _node_from_a, _node_to_a = get_bsdf_branch_indices( + assignment=assignment, + branches_locally=branches_local, + branch_direction_locally=direction_local, + from_node=from_node, + to_node=to_node, + ) + + ptdf_diff_raw = _compute_ptdf_difference( + ptdf=ptdf, + branch_from_a=branch_from_a, + branch_to_a=branch_to_a, + ) + ptdf_diff, _ptdf_b = _adjust_ptdf_for_slack( + ptdf_diff=ptdf_diff_raw, + is_slack=is_slack, + bus_a_ptdf=bus_a_ptdf, + bus_b_ptdf=bus_b_ptdf, + ) + + from_node_new = from_node.copy() + to_node_new = to_node.copy() + from_node_new[branch_from_b] = bus_b_ptdf + to_node_new[branch_to_b] = bus_b_ptdf + + active_nodes = np.unique(np.r_[from_node_new, to_node_new]) + node_remap = {old_idx: new_idx for new_idx, old_idx in enumerate(active_nodes.tolist())} + from_node_compact = np.array([node_remap[idx] for idx in from_node_new], dtype=int) + to_node_compact = np.array([node_remap[idx] for idx in to_node_new], dtype=int) + + slack_compact = node_remap[bus_a_ptdf] + ptdf_compact = compute_ptdf( + from_node=from_node_compact, + to_node=to_node_compact, + susceptances=susceptance, + slack_bus=slack_compact, + ) + + ptdf_new = ptdf.copy() + ptdf_new[:, active_nodes] = ptdf_compact + + delta_ptdf = ptdf_new - ptdf + ptdf_diff_norm = float(ptdf_diff @ ptdf_diff) + bsdf = delta_ptdf @ ptdf_diff / ptdf_diff_norm + reconstruction_error = float(np.max(np.abs(delta_ptdf - np.outer(bsdf, ptdf_diff)))) + + return bsdf, ptdf_new, from_node_new, to_node_new, reconstruction_error + + +def compute_flow_exact_rebuild( + switched_node: int, + assignment: Bool[np.ndarray, " n_branches_at_node"], + is_slack: bool, + bus_a_ptdf: int, + bus_b_ptdf: int, + ptdf: Float[np.ndarray, " n_branches n_nodes"], + susceptance: Float[np.ndarray, " n_branches"], + from_node: Int[np.ndarray, " n_branches"], + to_node: Int[np.ndarray, " n_branches"], + branches_at_nodes: list[Int[np.ndarray, " n_branches_at_node"]], + branch_direction: list[Bool[np.ndarray, " n_branches_at_node"]], + nodal_injection: Float[np.ndarray, " n_nodes"], +) -> tuple[ + Float[np.ndarray, " n_branches"], + Float[np.ndarray, " n_branches n_nodes"], + Int[np.ndarray, " n_branches"], + Int[np.ndarray, " n_branches"], + float, +]: + """Compute exact rebuilt split flows for a single bus split. + + This is a thin convenience wrapper around ``calc_bsdf_exact_rebuild`` for diagnostics that + need the post-split flow vector rather than only the rebuilt PTDF. + + Parameters + ---------- + switched_node : int + Index of the node being split. + assignment : Bool[np.ndarray, " n_branches_at_node"] + Boolean mask for branch assignment to busbars. + is_slack : bool + True if the node is the slack node. + bus_a_ptdf : int + PTDF column index for busbar A. + bus_b_ptdf : int + PTDF column index for busbar B. + ptdf : Float[np.ndarray, " n_branches n_nodes"] + PTDF matrix before the split. + susceptance : Float[np.ndarray, " n_branches"] + Branch susceptances. + from_node : Int[np.ndarray, " n_branches"] + Branch from-nodes. + to_node : Int[np.ndarray, " n_branches"] + Branch to-nodes. + branches_at_nodes : list[Int[np.ndarray, " n_branches_at_node"]] + Branch indices at each node. + branch_direction : list[Bool[np.ndarray, " n_branches_at_node"]] + Direction mask for branches at each node. + nodal_injection : Float[np.ndarray, " n_nodes"] + Nodal injection vector to apply to the rebuilt PTDF. + + Returns + ------- + Float[np.ndarray, " n_branches"] + Exact rebuilt branch flows after the split. + Float[np.ndarray, " n_branches n_nodes"] + Exact PTDF after the split on the active connected node set. + Int[np.ndarray, " n_branches"] + Updated from-nodes. + Int[np.ndarray, " n_branches"] + Updated to-nodes. + float + Maximum absolute reconstruction error of the projected rank-1 BSDF update. + """ + _bsdf, ptdf_new, from_node_new, to_node_new, reconstruction_error = calc_bsdf_exact_rebuild( + switched_node=switched_node, + assignment=assignment, + is_slack=is_slack, + bus_a_ptdf=bus_a_ptdf, + bus_b_ptdf=bus_b_ptdf, + ptdf=ptdf, + susceptance=susceptance, + from_node=from_node, + to_node=to_node, + branches_at_nodes=branches_at_nodes, + branch_direction=branch_direction, + ) + return ptdf_new @ nodal_injection, ptdf_new, from_node_new, to_node_new, reconstruction_error + + def extract_bsdf_data( topo_vect: Bool[np.ndarray, " n_branches_at_subs"], branches_at_nodes: list[Int[np.ndarray, " n_branches_at_node"]], diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py index 570221974..234beb4cd 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py @@ -33,9 +33,10 @@ def test_apply_station(case14_data_folder: Path) -> None: assert len(apply_diff.switches_created) == 1 assert len(apply_diff.busbars_deleted) == 0 assert len(apply_diff.switches_deleted) == 0 - assert len(realized_station.disconnection_diff) == 0 + assert len(realized_station.branch_disconnection_diff) == 0 + assert len(realized_station.injection_disconnection_diff) == 0 assert len(realized_station.coupler_diff) == 0 - assert len(realized_station.reassignment_diff) + assert len(realized_station.branch_reassignment_diff) + len(realized_station.injection_reassignment_diff) def test_apply_station_existing_buses(case14_data_folder: Path) -> None: @@ -56,9 +57,10 @@ def test_apply_station_existing_buses(case14_data_folder: Path) -> None: assert len(apply_diff.switches_created) == 0 assert len(apply_diff.busbars_deleted) == 0 assert len(apply_diff.switches_deleted) == 0 - assert len(realized_station.disconnection_diff) == 0 + assert len(realized_station.branch_disconnection_diff) == 0 + assert len(realized_station.injection_disconnection_diff) == 0 assert len(realized_station.coupler_diff) == 1 - assert len(realized_station.reassignment_diff) + assert len(realized_station.branch_reassignment_diff) + len(realized_station.injection_reassignment_diff) def test_apply_station_extra_busbar(case14_data_folder: Path) -> None: @@ -81,9 +83,9 @@ def test_apply_station_extra_busbar(case14_data_folder: Path) -> None: assert len(apply_diff.switches_created) == 0 assert apply_diff.busbars_deleted == [16] assert apply_diff.switches_deleted == [2] - assert len(realized_station.disconnection_diff) == 0 + assert len(realized_station.branch_disconnection_diff) == 0 + assert len(realized_station.injection_disconnection_diff) == 0 assert len(realized_station.coupler_diff) == 1 - assert len(realized_station.reassignment_diff) def test_apply_topology(case14_data_folder: Path) -> None: @@ -100,7 +102,8 @@ def test_apply_topology(case14_data_folder: Path) -> None: assert len(local_apply_diff.busbars_deleted) == 0 assert len(local_apply_diff.switches_deleted) == 0 - assert len(realized_topology.disconnection_diff) == 0 + assert len(realized_topology.branch_disconnection_diff) == 0 + assert len(realized_topology.injection_disconnection_diff) == 0 assert len(realized_topology.coupler_diff) == 0 - assert len(realized_topology.reassignment_diff) + assert len(realized_topology.branch_reassignment_diff) + len(realized_topology.injection_reassignment_diff) assert realized_topology.topology == asset_topo diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py index ced6a2cee..9a8085f3f 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py @@ -13,11 +13,13 @@ import pytest from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import ( apply_node_breaker_topology, - apply_single_asset_bus_branch, + apply_single_branch_bus_branch, + apply_single_injection_bus_branch, apply_station, apply_station_bus_branch, apply_topology_bus_branch, - find_asset, + find_branch, + find_injection, ) from toop_engine_grid_helpers.powsybl.example_grids import basic_node_breaker_network_powsybl from toop_engine_interfaces.asset_topology import ( @@ -46,8 +48,9 @@ def build_test_topology(reference_topology: Topology, raw_stations: list[RawStat return copy_topology_with_updates( reference_topology=reference_topology, raw_stations=raw_stations, - assets=reference_topology.assets, asset_bays=reference_topology.asset_bays, + branch_assets=reference_topology.branch_assets, + injection_assets=reference_topology.injection_assets, ) @@ -59,126 +62,171 @@ def test_find_asset(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: bus_id = station.grid_model_id vl_id = net.get_buses().loc[bus_id]["voltage_level_id"] - for elem in [asset_connection.asset for asset_connection in station.asset_connections]: - is_branch, connected, direction, bus_breaker_id = find_asset( - net=net, - elem_id=elem.grid_model_id, - voltage_level_id=vl_id, - bus_id=bus_id, + for asset_connection in station.branch_connections: + elem = asset_connection.asset + connected, direction, bus_breaker_id = find_branch( + net=net, elem_id=elem.grid_model_id, voltage_level_id=vl_id, bus_id=bus_id ) - - assert is_branch == elem.is_branch() assert connected - if is_branch: - brh = net.get_branches(all_attributes=True).loc[elem.grid_model_id] - if direction: - assert brh["bus1_id"] == bus_id - assert brh["bus_breaker_bus1_id"] == bus_breaker_id - else: - assert brh["bus2_id"] == bus_id - assert brh["bus_breaker_bus2_id"] == bus_breaker_id - else: - inj = net.get_injections(all_attributes=True).loc[elem.grid_model_id] - assert inj["bus_id"] == bus_id - assert inj["bus_breaker_bus_id"] == bus_breaker_id - - for elem in [asset_connection.asset for asset_connection in topology.materialize_stations()[10].asset_connections]: - if elem.is_branch(): - with pytest.raises(ValueError, match=f"Branch {elem.grid_model_id} not found in the station"): - find_asset( - net=net, - elem_id=elem.grid_model_id, - voltage_level_id=vl_id, - bus_id=bus_id, - ) + brh = net.get_branches(all_attributes=True).loc[elem.grid_model_id] + if direction: + assert brh["bus1_id"] == bus_id + assert brh["bus_breaker_bus1_id"] == bus_breaker_id else: - with pytest.raises(ValueError, match=f"Element {elem.grid_model_id} not found in the station."): - find_asset( - net=net, - elem_id=elem.grid_model_id, - voltage_level_id=vl_id, - bus_id=bus_id, - ) + assert brh["bus2_id"] == bus_id + assert brh["bus_breaker_bus2_id"] == bus_breaker_id + + for asset_connection in station.injection_connections: + elem = asset_connection.asset + connected, bus_breaker_id = find_injection(net=net, elem_id=elem.grid_model_id, voltage_level_id=vl_id) + assert connected + inj = net.get_injections(all_attributes=True).loc[elem.grid_model_id] + assert inj["bus_id"] == bus_id + assert inj["bus_breaker_bus_id"] == bus_breaker_id + + for elem in [asset_connection.asset for asset_connection in topology.materialize_stations()[10].branch_connections]: + with pytest.raises(ValueError, match=f"Branch {elem.grid_model_id} not found in the station"): + find_branch( + net=net, + elem_id=elem.grid_model_id, + voltage_level_id=vl_id, + bus_id=bus_id, + ) + + for elem in [asset_connection.asset for asset_connection in topology.materialize_stations()[10].injection_connections]: + with pytest.raises(ValueError, match=f"Element {elem.grid_model_id} not found in the station."): + find_injection( + net=net, + elem_id=elem.grid_model_id, + voltage_level_id=vl_id, + ) def test_apply_single_asset_bus_branch_reassign(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: grid_path, topology = case14_data_with_asset_topo - net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) base_station = topology.materialize_stations()[0] - for asset_index in range(len(base_station.asset_connections)): + for asset_index in range(len(base_station.branch_connections)): + net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) station = base_station.model_copy(deep=True) - # Reassign the first asset to the second bus assert len(station.busbars) == 2 - station.asset_switching_table[:, asset_index] = ~station.asset_switching_table[:, asset_index] - status, reassignments = apply_single_asset_bus_branch( + current_state = station.branch_switching_table[:, asset_index] + station.branch_switching_table[:, asset_index] = ~current_state + disconnections, reassignments = apply_single_branch_bus_branch( net=net, station=station, - asset_index=asset_index, ) - assert status == "reassigned" + assert disconnections == [] assert len(reassignments) == 2 for re_asset_index, bus_id, connected in reassignments: assert re_asset_index == asset_index - assert connected == station.asset_switching_table[bus_id, asset_index] + assert connected == station.branch_switching_table[bus_id, asset_index] new_bus = [ bus - for (bus, switching) in zip(station.busbars, station.asset_switching_table[:, asset_index].tolist(), strict=True) + for (bus, switching) in zip( + station.busbars, station.branch_switching_table[:, asset_index].tolist(), strict=True + ) if switching ] assert len(new_bus) == 1 new_bus = new_bus[0] - asset = station.asset_connections[asset_index].asset - if asset.is_branch(): - brh = net.get_branches(all_attributes=True).loc[asset.grid_model_id] - assert brh["bus_breaker_bus1_id"] == new_bus.grid_model_id or brh["bus_breaker_bus2_id"] == new_bus.grid_model_id - else: - inj = net.get_injections(all_attributes=True).loc[asset.grid_model_id] - assert inj["bus_breaker_bus_id"] == new_bus.grid_model_id + asset = station.branch_connections[asset_index].asset + brh = net.get_branches(all_attributes=True).loc[asset.grid_model_id] + assert brh["bus_breaker_bus1_id"] == new_bus.grid_model_id or brh["bus_breaker_bus2_id"] == new_bus.grid_model_id + + for asset_index in range(len(base_station.injection_connections)): + net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) + station = base_station.model_copy(deep=True) + assert len(station.busbars) == 2 + current_state = station.injection_switching_table[:, asset_index] + station.injection_switching_table[:, asset_index] = ~current_state + disconnections, reassignments = apply_single_injection_bus_branch( + net=net, + station=station, + ) + assert disconnections == [] + assert len(reassignments) == 2 + for re_asset_index, bus_id, connected in reassignments: + assert re_asset_index == asset_index + assert connected == station.injection_switching_table[bus_id, asset_index] + + new_bus = [ + bus + for (bus, switching) in zip( + station.busbars, station.injection_switching_table[:, asset_index].tolist(), strict=True + ) + if switching + ] + assert len(new_bus) == 1 + new_bus = new_bus[0] + asset = station.injection_connections[asset_index].asset + inj = net.get_injections(all_attributes=True).loc[asset.grid_model_id] + assert inj["bus_breaker_bus_id"] == new_bus.grid_model_id def test_apply_single_asset_bus_branch_disconnect(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: grid_path, topology = case14_data_with_asset_topo - net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) base_station = topology.materialize_stations()[0] - for asset_index in range(len(base_station.asset_connections)): + for asset_index in range(len(base_station.branch_connections)): + net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) station = base_station.model_copy(deep=True) # Disconnect the first asset assert len(station.busbars) == 2 - station.asset_switching_table[:, asset_index] = False - status, reassignments = apply_single_asset_bus_branch( + station.branch_switching_table[:, asset_index] = False + disconnections, reassignments = apply_single_branch_bus_branch( net=net, station=station, - asset_index=asset_index, ) - assert status == "disconnected" + assert disconnections == [asset_index] assert len(reassignments) == 0 - asset = station.asset_connections[asset_index].asset - if asset.is_branch(): - brh = net.get_branches(all_attributes=True).loc[asset.grid_model_id] - assert not brh["connected1"] and not brh["connected2"] - else: - inj = net.get_injections(all_attributes=True).loc[asset.grid_model_id] - assert not inj["connected"] + asset = station.branch_connections[asset_index].asset + brh = net.get_branches(all_attributes=True).loc[asset.grid_model_id] + assert not brh["connected1"] and not brh["connected2"] -def test_apply_single_asset_bus_branch_nothing(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: +def test_apply_single_asset_bus_injection_disconnect(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: grid_path, topology = case14_data_with_asset_topo - net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) base_station = topology.materialize_stations()[0] - for asset_index in range(len(base_station.asset_connections)): - status, reassignments = apply_single_asset_bus_branch( + for asset_index in range(len(base_station.injection_connections)): + net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) + station = base_station.model_copy(deep=True) + assert len(station.busbars) == 2 + station.injection_switching_table[:, asset_index] = False + disconnections, reassignments = apply_single_injection_bus_branch( net=net, - station=base_station, - asset_index=asset_index, + station=station, ) - assert status == "nothing" + assert disconnections == [asset_index] assert len(reassignments) == 0 + asset = station.injection_connections[asset_index].asset + inj = net.get_injections(all_attributes=True).loc[asset.grid_model_id] + assert not inj["connected"] + + +def test_apply_single_asset_bus_branch_nothing(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: + grid_path, topology = case14_data_with_asset_topo + net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) + + base_station = topology.materialize_stations()[0] + branch_disconnections, branch_reassignments = apply_single_branch_bus_branch( + net=net, + station=base_station, + ) + assert branch_disconnections == [] + assert branch_reassignments == [] + + injection_disconnections, injection_reassignments = apply_single_injection_bus_branch( + net=net, + station=base_station, + ) + assert injection_disconnections == [] + assert injection_reassignments == [] + def test_apply_station_bus_branch_reassign(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: grid_path, topology = case14_data_with_asset_topo @@ -191,23 +239,30 @@ def test_apply_station_bus_branch_reassign(case14_data_with_asset_topo: tuple[Pa station=station, ) assert len(realized_station.coupler_diff) == 0 - assert len(realized_station.reassignment_diff) == 0 - assert len(realized_station.disconnection_diff) == 0 + assert len(realized_station.branch_reassignment_diff) == 0 + assert len(realized_station.injection_reassignment_diff) == 0 + assert len(realized_station.branch_disconnection_diff) == 0 + assert len(realized_station.injection_disconnection_diff) == 0 realized_station_2 = apply_station(net=net, topology=topology, raw_station=topology.raw_stations[0]) assert realized_station_2 == realized_station # Try with disconnections and reassignments - assert np.array_equal(station.asset_switching_table, np.array([[True, True, True], [False, False, False]])) + assert np.array_equal( + np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1), + np.array([[True, True, True], [False, False, False]]), + ) - station.asset_switching_table = np.array([[True, False, False], [False, True, False]]) + target_switching_table = np.array([[True, False, False], [False, True, False]]) + station.branch_switching_table = target_switching_table[:, : len(station.branch_connections)] + station.injection_switching_table = target_switching_table[:, len(station.branch_connections) :] realized_station = apply_station_bus_branch( net=net, station=station, ) assert len(realized_station.coupler_diff) == 0 - assert len(realized_station.reassignment_diff) == 2 - assert len(realized_station.disconnection_diff) == 1 + assert len(realized_station.branch_reassignment_diff) + len(realized_station.injection_reassignment_diff) == 2 + assert len(realized_station.branch_disconnection_diff) + len(realized_station.injection_disconnection_diff) == 1 def test_apply_station_bus_branch_coupler(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: @@ -215,7 +270,8 @@ def test_apply_station_bus_branch_coupler(case14_data_with_asset_topo: tuple[Pat net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) station = topology.materialize_stations()[0].model_copy(deep=True) - station.asset_switching_table = np.array([[True, False, True], [False, True, False]]) + station.branch_switching_table = np.array([[True, False], [False, True]]) + station.injection_switching_table = np.array([[True], [False]]) station.couplers[0].open = True realized_station = apply_station_bus_branch( @@ -224,8 +280,8 @@ def test_apply_station_bus_branch_coupler(case14_data_with_asset_topo: tuple[Pat ) assert len(realized_station.coupler_diff) == 1 assert realized_station.coupler_diff[0] == station.couplers[0] - assert len(realized_station.reassignment_diff) == 2 - assert len(realized_station.disconnection_diff) == 0 + assert len(realized_station.branch_reassignment_diff) + len(realized_station.injection_reassignment_diff) == 2 + assert len(realized_station.branch_disconnection_diff) + len(realized_station.injection_disconnection_diff) == 0 def test_apply_node_breaker_topology(basic_node_breaker_topology: Topology) -> None: @@ -266,8 +322,10 @@ def test_apply_topology_bus_branch_do_nothing(case14_data_with_asset_topo: tuple topology=topology, ) assert len(realized_topology.coupler_diff) == 0 - assert len(realized_topology.reassignment_diff) == 0 - assert len(realized_topology.disconnection_diff) == 0 + assert len(realized_topology.branch_reassignment_diff) == 0 + assert len(realized_topology.injection_reassignment_diff) == 0 + assert len(realized_topology.branch_disconnection_diff) == 0 + assert len(realized_topology.injection_disconnection_diff) == 0 def test_apply_topology_bus_branch_reassign(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: @@ -279,7 +337,12 @@ def test_apply_topology_bus_branch_reassign(case14_data_with_asset_topo: tuple[P # Just randomize the switching tables, we'll have both disconnections and reassignments then raw_stations = [ station.model_copy( - update={"asset_switching_table": np.random.randint(0, 2, size=station.asset_switching_table.shape, dtype=bool)} + update={ + "branch_switching_table": np.random.randint(0, 2, size=station.branch_switching_table.shape, dtype=bool), + "injection_switching_table": np.random.randint( + 0, 2, size=station.injection_switching_table.shape, dtype=bool + ), + } ) for station in topology.raw_stations ] @@ -291,5 +354,5 @@ def test_apply_topology_bus_branch_reassign(case14_data_with_asset_topo: tuple[P topology=topology, ) assert len(realized_topology.coupler_diff) == 0 - assert len(realized_topology.reassignment_diff) > 0 - assert len(realized_topology.disconnection_diff) > 0 + assert len(realized_topology.branch_reassignment_diff) + len(realized_topology.injection_reassignment_diff) > 0 + assert len(realized_topology.branch_disconnection_diff) + len(realized_topology.injection_disconnection_diff) > 0 diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py b/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py index fedc5db1d..7e4dc7b15 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py @@ -24,13 +24,17 @@ convert_topo_to_action_set_index, default_topology, ) +from toop_engine_dc_solver.jax.topology_looper import run_solver_symmetric +from toop_engine_dc_solver.jax.types import ActionIndexComputations from toop_engine_dc_solver.postprocess.postprocess_pandapower import ( compute_n_1_dc, ) -from toop_engine_dc_solver.preprocess.convert_to_jax import convert_to_jax +from toop_engine_dc_solver.postprocess.postprocess_powsybl import PowsyblRunner +from toop_engine_dc_solver.preprocess.convert_to_jax import convert_to_jax, load_grid from toop_engine_dc_solver.preprocess.helpers.find_bridges import ( find_n_minus_2_safe_branches, ) +from toop_engine_dc_solver.preprocess.network_data import extract_action_set, 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_grid_helpers.pandapower.pandapower_helpers import ( @@ -40,10 +44,79 @@ get_pandapower_loadflow_results_injection, ) from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import table_id +from toop_engine_grid_helpers.powsybl.loadflow_parameters import DISTRIBUTED_SLACK from toop_engine_interfaces.folder_structure import ( CHRONICS_FILE_NAMES, PREPROCESSING_PATHS, ) +from toop_engine_interfaces.loadflow_result_helpers_polars import extract_solver_matrices_polars +from toop_engine_interfaces.messages.preprocess.preprocess_commands import PreprocessParameters +from toop_engine_interfaces.stored_action_set import ActionSet + + +def _select_asset_covering_action_indices(action_set: ActionSet) -> list[int]: + """Select a small action subset covering all changed branch and injection entries. + + The action set is grouped by station. For each station we greedily choose local + actions until every branch or injection column that changes relative to the + simplified starting topology is covered at least once. + """ + starting_station_by_id = { + station.grid_model_id: station for station in action_set.simplified_starting_topology.materialize_stations() + } + selected_action_indices: list[int] = [] + action_idx = 0 + + while action_idx < len(action_set.local_actions): + station_id = action_set.local_actions[action_idx].grid_model_id + station_start = action_idx + while ( + action_idx < len(action_set.local_actions) and action_set.local_actions[action_idx].grid_model_id == station_id + ): + action_idx += 1 + station_end = action_idx + + starting_station = starting_station_by_id[station_id] + station_actions = action_set.local_actions[station_start:station_end] + changed_assets_by_action = [] + for station in station_actions: + changed_branch_mask = np.any( + station.branch_switching_table != starting_station.branch_switching_table, + axis=0, + ) + changed_injection_mask = np.any( + station.injection_switching_table != starting_station.injection_switching_table, + axis=0, + ) + changed_assets_by_action.append( + { + *(set(("branch", asset_idx) for asset_idx in np.flatnonzero(changed_branch_mask).tolist())), + *(set(("injection", asset_idx) for asset_idx in np.flatnonzero(changed_injection_mask).tolist())), + } + ) + + uncovered_assets = set().union(*changed_assets_by_action) + chosen_station_actions: set[int] = set() + + while uncovered_assets: + best_action_offset = None + best_covered_assets: set[tuple[str, int]] = set() + for offset, changed_assets in enumerate(changed_assets_by_action): + if offset in chosen_station_actions: + continue + covered_assets = changed_assets & uncovered_assets + if len(covered_assets) > len(best_covered_assets): + best_action_offset = offset + best_covered_assets = covered_assets + + if best_action_offset is None or not best_covered_assets: + break + + chosen_station_actions.add(best_action_offset) + selected_action_indices.append(station_start + best_action_offset) + uncovered_assets -= best_covered_assets + + return selected_action_indices def test_grid_loadflow_uptodate(data_folder: Path) -> None: @@ -81,6 +154,71 @@ def test_n_0_results(data_folder: Path, preprocessed_data_folder: Path) -> None: assert abs_backend_loadflow.shape == n_0[0, 0, :].shape +def test_powsybl_complex_grid_actions_match_runner( + complex_grid_battery_hvdc_svc_3w_trafo_linear_1_0_data_folder: Path, +) -> None: + _, static_information, network_data = load_grid( + data_folder_dirfs=DirFileSystem(str(complex_grid_battery_hvdc_svc_3w_trafo_linear_1_0_data_folder)), + parameters=PreprocessParameters(), + lf_params=DISTRIBUTED_SLACK, + ) + + action_set = extract_action_set(network_data) + nminus1_definition = extract_nminus1_definition(network_data) + + runner = PowsyblRunner(lf_params=DISTRIBUTED_SLACK) + runner.load_base_grid( + complex_grid_battery_hvdc_svc_3w_trafo_linear_1_0_data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"] + ) + runner.store_action_set(action_set) + runner.store_nminus1_definition(nminus1_definition) + + selected_action_indices = _select_asset_covering_action_indices(action_set) + failing_actions: list[str] = [] + + for action_idx in selected_action_indices: + actions = ActionIndexComputations( + action=jnp.array([[action_idx]], dtype=int), + pad_mask=jnp.array([True]), + ) + (n_0_solver, n_1_solver), success = run_solver_symmetric( + topologies=actions, + disconnections=None, + injections=None, + dynamic_information=static_information.dynamic_information, + solver_config=static_information.solver_config, + aggregate_output_fn=lambda lf_res: (lf_res.n_0_matrix, lf_res.n_1_matrix), + ) + + assert np.all(success) + + runner_result = runner.run_dc_loadflow([action_idx], []) + n_0_runner, n_1_runner, runner_success = extract_solver_matrices_polars(runner_result, nminus1_definition, 0) + + assert np.all(runner_success) + + n_0_expected = np.abs(np.asarray(n_0_solver[0, 0])) + n_1_expected = np.abs(np.asarray(n_1_solver[0, 0])) + n_0_actual = np.abs(n_0_runner) + n_1_actual = np.abs(n_1_runner) + + n_0_matches = np.allclose(n_0_expected, n_0_actual) + n_1_matches = np.allclose(n_1_expected, n_1_actual) + if n_0_matches and n_1_matches: + continue + + failure_parts = [] + if not n_0_matches: + n_0_diff = np.abs(n_0_expected - n_0_actual) + failure_parts.append(f"N-0 max_diff={float(np.max(n_0_diff)):.6g} mean_diff={float(np.mean(n_0_diff)):.6g}") + if not n_1_matches: + n_1_diff = np.abs(n_1_expected - n_1_actual) + failure_parts.append(f"N-1 max_diff={float(np.max(n_1_diff)):.6g} mean_diff={float(np.mean(n_1_diff)):.6g}") + failing_actions.append(f"action {action_idx}: {'; '.join(failure_parts)}") + + assert not failing_actions, "Failing actions:\n" + "\n".join(failing_actions) + + @pytest.mark.skip( reason="known issue: trafo3w sides (hv/mv/lv) in monitored elements unique_id (e.g. 12%%trafo3w_lv), causing incorrect/duplicated results" ) diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py index 98e7ad2e2..d6a9b8d27 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py @@ -599,8 +599,9 @@ def test_enumerate_station_realisations_no_coupler( asset_topology=copy_topology_with_updates( reference_topology=network_data_filled.asset_topology, raw_stations=raw_stations, - assets=network_data_filled.asset_topology.assets, asset_bays=network_data_filled.asset_topology.asset_bays, + branch_assets=network_data_filled.asset_topology.branch_assets, + injection_assets=network_data_filled.asset_topology.injection_assets, ), ) network_data = filter_relevant_nodes_branch_count(network_data) @@ -627,7 +628,10 @@ def test_simplify_asset_topology( MaterializedStation.model_validate(station) branch_ids = [network_data.branch_ids[i] for i in network_data.branches_at_nodes[rel_node_index]] inj_ids = [network_data.injection_ids[i] for i in network_data.injection_idx_at_nodes[rel_node_index]] - asset_ids = [a.grid_model_id for a in station.assets] + asset_ids = [ + *(asset_connection.asset.grid_model_id for asset_connection in station.branch_connections), + *(asset_connection.asset.grid_model_id for asset_connection in station.injection_connections), + ] assert branch_ids == asset_ids[: len(branch_ids)] assert inj_ids == asset_ids[len(branch_ids) :] @@ -890,7 +894,10 @@ def test_preprocess(data_folder: str, tmp_path: str) -> None: MaterializedStation.model_validate(station) branch_ids = [network_data.branch_ids[i] for i in network_data.branches_at_nodes[rel_node_index]] inj_ids = [network_data.injection_ids[i] for i in network_data.injection_idx_at_nodes[rel_node_index]] - asset_ids = [a.grid_model_id for a in station.assets] + asset_ids = [ + *(asset_connection.asset.grid_model_id for asset_connection in station.branch_connections), + *(asset_connection.asset.grid_model_id for asset_connection in station.injection_connections), + ] assert branch_ids == asset_ids[: len(branch_ids)] assert inj_ids == asset_ids[len(branch_ids) :] diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py index 95008e274..32518b1e7 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py @@ -26,7 +26,21 @@ update_network_data_with_non_rel_bb_outages, ) from toop_engine_dc_solver.preprocess.preprocess_station_realisations import enumerate_station_realisations -from toop_engine_interfaces.asset_topology import Busbar, MaterializedAssetConnection, MaterializedStation, SwitchableAsset +from toop_engine_interfaces.asset_topology import ( + BranchAsset, + Busbar, + InjectionAsset, + MaterializedAssetConnection, + MaterializedStation, +) + + +def _combined_asset_connections(station: MaterializedStation) -> list[MaterializedAssetConnection]: + return [*station.branch_connections, *station.injection_connections] + + +def _combined_asset_switching_table(station: MaterializedStation) -> np.ndarray: + return np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1) def test_get_total_injection_along_stub_branch(network_data: NetworkData): @@ -72,9 +86,9 @@ def test_get_total_injection_along_stub_branch(network_data: NetworkData): def test_extract_outage_index_injection_from_asset(network_data: NetworkData): # Create mock SwitchableAsset objects - asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, type="line") - asset2 = SwitchableAsset(grid_model_id="branch_12", in_service=False, type="line") - asset3 = SwitchableAsset(grid_model_id="branch_23", in_service=True, type="line") + asset1 = BranchAsset(grid_model_id="branch_01", in_service=True, asset_type="line") + asset2 = BranchAsset(grid_model_id="branch_12", in_service=False, asset_type="line") + asset3 = BranchAsset(grid_model_id="branch_23", in_service=True, asset_type="line") # asset4 = SwitchableAsset( # grid_model_id="branch_02", in_service=True, branch_end="from" # ) @@ -84,10 +98,10 @@ def test_extract_outage_index_injection_from_asset(network_data: NetworkData): # asset6 = SwitchableAsset( # grid_model_id="injection_node_0", in_service=True, branch_end=None # ) - asset7 = SwitchableAsset( + asset7 = InjectionAsset( grid_model_id="injection_node_2", in_service=True, - type="GENERATOR", + asset_type="GENERATOR", ) # asset8 = SwitchableAsset( # grid_model_id="injection_node_1", in_service=True, branch_end=None @@ -177,20 +191,20 @@ def test_extract_outage_index_injection_from_asset(network_data: NetworkData): def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): # Create mock SwitchableAsset objects - asset1 = SwitchableAsset(grid_model_id="branch_01", in_service=True, type="line") - asset2 = SwitchableAsset(grid_model_id="branch_12", in_service=False, type="line") - asset3 = SwitchableAsset(grid_model_id="branch_23", in_service=True, type="line") - asset4 = SwitchableAsset(grid_model_id="branch_02", in_service=True, type="line") - asset5 = SwitchableAsset(grid_model_id="branch_03", in_service=True, type="line") - asset6 = SwitchableAsset( + asset1 = BranchAsset(grid_model_id="branch_01", in_service=True, asset_type="line") + asset2 = BranchAsset(grid_model_id="branch_12", in_service=False, asset_type="line") + asset3 = BranchAsset(grid_model_id="branch_23", in_service=True, asset_type="line") + asset4 = BranchAsset(grid_model_id="branch_02", in_service=True, asset_type="line") + asset5 = BranchAsset(grid_model_id="branch_03", in_service=True, asset_type="line") + asset6 = InjectionAsset( grid_model_id="injection_node_0", in_service=True, - type="GENERATOR", + asset_type="GENERATOR", ) - asset7 = SwitchableAsset( + asset7 = InjectionAsset( grid_model_id="injection_node_2", in_service=True, - type="GENERATOR", + asset_type="GENERATOR", ) # asset8 = SwitchableAsset( # grid_model_id="injection_node_1", in_service=True, branch_end=None @@ -235,11 +249,19 @@ def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): grid_model_id="node_2", busbars=[busbar_0, busbar_1], couplers=[], - asset_connections=[MaterializedAssetConnection(asset=asset) for asset in [asset2, asset3, asset4, asset7]], - asset_switching_table=np.array( + branch_connections=[MaterializedAssetConnection(asset=asset) for asset in [asset2, asset3, asset4]], + injection_connections=[MaterializedAssetConnection(asset=asset7)], + branch_switching_table=np.array( + [ + [True, False, True], # Busbar 0 + [False, True, False], # Busbar 1 + ], + dtype=bool, + ), + injection_switching_table=np.array( [ - [True, False, True, False], # Busbar 0 - [False, True, False, True], # Busbar 1 + [False], + [True], ], dtype=bool, ), @@ -326,10 +348,17 @@ def test_extract_busbar_outage_data(network_data_preprocessed: NetworkData): grid_model_id="node_0", busbars=[busbar_0], couplers=[], - asset_connections=[MaterializedAssetConnection(asset=asset) for asset in [asset1, asset4, asset5, asset6]], - asset_switching_table=np.array( + branch_connections=[MaterializedAssetConnection(asset=asset) for asset in [asset1, asset4, asset5]], + injection_connections=[MaterializedAssetConnection(asset=asset6)], + branch_switching_table=np.array( [ - [True, True, True, True], # Busbar 0 + [True, True, True], # Busbar 0 + ], + dtype=bool, + ), + injection_switching_table=np.array( + [ + [True], ], dtype=bool, ), @@ -393,11 +422,11 @@ def test_update_network_data_with_non_rel_bb_outages(network_data_preprocessed: for branch_index in branch_outages: branch_id = updated_net_data.branch_ids[branch_index] # get asset_index of the branch - for asset_index, asset_connection in enumerate(station.asset_connections): + for asset_index, asset_connection in enumerate(_combined_asset_connections(station)): if asset_connection.asset.grid_model_id == branch_id: break - assert station.asset_switching_table[busbar_index, asset_index], ( + assert _combined_asset_switching_table(station)[busbar_index, asset_index], ( f"Branch {branch_id} is not connected to busbar {busbar_id}" ) @@ -508,13 +537,14 @@ def test_get_modified_stations(network_data_preprocessed: NetworkData): if not action.any(): res.append( np.all( - modified_stations_br[1][action_index].asset_switching_table == monitored_station.asset_switching_table + _combined_asset_switching_table(modified_stations_br[1][action_index]) + == _combined_asset_switching_table(monitored_station) ) ) else: res.append( np.all( - modified_stations_br[1][action_index].asset_switching_table[:, 0 : len(action)] == action, axis=1 + modified_stations_br[1][action_index].branch_switching_table[:, 0 : len(action)] == action, axis=1 ).any() ) assert np.sum(res) == len(modified_stations_br[1]), ( @@ -522,10 +552,7 @@ def test_get_modified_stations(network_data_preprocessed: NetworkData): ) assert np.all( [ - np.all( - modified_stations_br[1][i].asset_switching_table[:, len(branch_actions_all_rel_sub[1][i]) :] - == monitored_station.asset_switching_table[:, len(branch_actions_all_rel_sub[1][i]) :] - ) + np.all(modified_stations_br[1][i].injection_switching_table == monitored_station.injection_switching_table) for i in range(len(modified_stations_br[1])) ] ), ( diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py index 97c744a23..c8265338c 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py @@ -33,6 +33,14 @@ ) +def _combined_asset_connections(station: MaterializedStation) -> list[MaterializedAssetConnection]: + return [*station.branch_connections, *station.injection_connections] + + +def _combined_asset_switching_table(station: MaterializedStation) -> np.ndarray: + return np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1) + + def build_materialized_station( grid_model_id: str, busbars: list[Busbar], @@ -44,8 +52,10 @@ def build_materialized_station( grid_model_id=grid_model_id, busbars=busbars, couplers=couplers, - asset_connections=[MaterializedAssetConnection(asset=asset) for asset in assets], - asset_switching_table=asset_switching_table, + branch_connections=[MaterializedAssetConnection(asset=asset) for asset in assets], + injection_connections=[], + branch_switching_table=asset_switching_table, + injection_switching_table=np.zeros((asset_switching_table.shape[0], 0), dtype=bool), ) @@ -87,7 +97,7 @@ def test_make_configurations_table(): assert problems.disconnected_busbars is None assert problems.duplicate_couplers is None assert preprocessed_station.busbars == station.busbars - assert preprocessed_station.asset_connections == station.asset_connections + assert preprocessed_station.branch_connections == station.branch_connections assert len(preprocessed_station.couplers) == 3 assert all([not coupler.open for coupler in preprocessed_station.couplers]) @@ -97,7 +107,7 @@ def test_make_configurations_table(): assert coupler_states.shape == (3, 3) assert len(assignment) == 3 - table = preprocessed_station.asset_switching_table + table = preprocessed_station.branch_switching_table # Coupler 3 open config_1_a = table[0] | table[1] | table[2] @@ -200,7 +210,9 @@ def test_preprocess_station() -> None: def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> None: - num_assets_before = sum(len(station.asset_connections) for station in network_data.asset_topology.materialize_stations()) + num_assets_before = sum( + len(station.branch_connections) for station in network_data.asset_topology.materialize_stations() + ) topo = add_missing_asset_topology_branch_info( asset_topology=network_data.asset_topology, @@ -214,11 +226,11 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No from_ends = 0 to_ends = 0 for station in topo.materialize_stations(): - for asset_connection in station.asset_connections: + for asset_connection in station.branch_connections: asset = asset_connection.asset if asset.grid_model_id in network_data.branch_ids: assert asset.name in network_data.branch_names - assert asset.type in network_data.branch_types + assert asset.asset_type in network_data.branch_types assert asset_connection.terminal in ["from", "to"] if asset_connection.terminal == "from": from_ends += 1 @@ -228,12 +240,15 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No assert from_ends > 0 assert to_ends > 0 - num_assets_after = sum(len(station.asset_connections) for station in topo.materialize_stations()) + num_assets_after = sum(len(station.branch_connections) for station in topo.materialize_stations()) assert num_assets_before == num_assets_after def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> None: - num_assets_before = sum(len(station.asset_connections) for station in network_data.asset_topology.materialize_stations()) + num_assets_before = sum( + len(_combined_asset_connections(station)) for station in network_data.asset_topology.materialize_stations() + ) + replacement_injection_type = "load" topo = add_missing_asset_topology_injection_info( asset_topology=network_data.asset_topology, @@ -244,12 +259,12 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> ) for station in topo.materialize_stations(): - for asset in [asset_connection.asset for asset_connection in station.asset_connections]: + for asset in [asset_connection.asset for asset_connection in _combined_asset_connections(station)]: if asset.grid_model_id in network_data.injection_ids: assert asset.name in network_data.injection_names - assert asset.type in network_data.injection_types + assert asset.asset_type in network_data.injection_types - num_assets_after = sum(len(station.asset_connections) for station in topo.materialize_stations()) + num_assets_after = sum(len(_combined_asset_connections(station)) for station in topo.materialize_stations()) assert num_assets_before == num_assets_after # Test with overwrite_if_present=False @@ -257,29 +272,29 @@ def test_add_missing_asset_topology_injection_info(network_data: NetworkData) -> asset_topology=topo, injection_ids=network_data.injection_ids, injection_names=["new_name"] * len(network_data.injection_ids), - injection_types=["new_type"] * len(network_data.injection_ids), + injection_types=[replacement_injection_type] * len(network_data.injection_ids), overwrite_if_present=False, ) for station in topo.materialize_stations(): - for asset in [asset_connection.asset for asset_connection in station.asset_connections]: + for asset in [asset_connection.asset for asset_connection in _combined_asset_connections(station)]: if asset.grid_model_id in network_data.injection_ids: assert asset.name in network_data.injection_names - assert asset.type in network_data.injection_types + assert asset.asset_type in network_data.injection_types topo = add_missing_asset_topology_injection_info( asset_topology=topo, injection_ids=network_data.injection_ids, injection_names=["new_name"] * len(network_data.injection_ids), - injection_types=["new_type"] * len(network_data.injection_ids), + injection_types=[replacement_injection_type] * len(network_data.injection_ids), overwrite_if_present=True, ) for station in topo.materialize_stations(): - for asset in [asset_connection.asset for asset_connection in station.asset_connections]: + for asset in [asset_connection.asset for asset_connection in _combined_asset_connections(station)]: if asset.grid_model_id in network_data.injection_ids: assert asset.name == "new_name" - assert asset.type == "new_type" + assert asset.asset_type == replacement_injection_type def test_prepare_for_separation_set_node_breaker_test_station(): @@ -287,20 +302,39 @@ def test_prepare_for_separation_set_node_breaker_test_station(): with open(file, "r") as f: station_data = json.load(f) + station_data["station_type"] = station_data.pop("type") + for busbar in station_data["busbars"]: + busbar["busbar_type"] = busbar.pop("type") + for coupler in station_data["couplers"]: + coupler["coupler_type"] = coupler.pop("type") + assets = station_data.pop("assets") + for asset in assets: + asset["asset_type"] = asset.pop("type") + branch_mask = [asset["asset_type"] in get_args(AssetBranchType) for asset in assets] + injection_mask = [asset["asset_type"] in get_args(AssetInjectionType) for asset in assets] asset_terminals = station_data.pop("asset_terminals", [None] * len(assets)) asset_bays = station_data.pop("asset_bays", [None] * len(assets)) - station_data["asset_connections"] = [ + combined_switching_table = np.asarray(station_data.pop("asset_switching_table"), dtype=bool) + station_data["branch_connections"] = [ + {"asset": asset, "terminal": asset_terminal, "asset_bay": asset_bay} + for asset, asset_terminal, asset_bay in zip(assets, asset_terminals, asset_bays, strict=True) + if asset["asset_type"] in get_args(AssetBranchType) + ] + station_data["injection_connections"] = [ {"asset": asset, "terminal": asset_terminal, "asset_bay": asset_bay} for asset, asset_terminal, asset_bay in zip(assets, asset_terminals, asset_bays, strict=True) + if asset["asset_type"] in get_args(AssetInjectionType) ] + station_data["branch_switching_table"] = combined_switching_table[:, branch_mask] + station_data["injection_switching_table"] = combined_switching_table[:, injection_mask] station = MaterializedStation.model_validate(station_data) x = nx.Graph() for busbar in station.busbars: x.add_node(busbar.int_id) for coupler in station.couplers: - if coupler.type != "DISCONNECTOR": + if coupler.coupler_type != "DISCONNECTOR": continue x.add_edge(coupler.busbar_from_id, coupler.busbar_to_id) connected_components = list(nx.connected_components(x)) @@ -311,13 +345,13 @@ def test_prepare_for_separation_set_node_breaker_test_station(): station, branch_ids=[ asset_connection.asset.grid_model_id - for asset_connection in station.asset_connections - if asset_connection.asset.type in get_args(AssetBranchType) + for asset_connection in _combined_asset_connections(station) + if asset_connection.asset.asset_type in get_args(AssetBranchType) ], injection_ids=[ asset_connection.asset.grid_model_id - for asset_connection in station.asset_connections - if asset_connection.asset.type in get_args(AssetInjectionType) + for asset_connection in _combined_asset_connections(station) + if asset_connection.asset.asset_type in get_args(AssetInjectionType) ], ) @@ -331,13 +365,13 @@ def test_prepare_for_separation_set_node_breaker_test_station(): station, branch_ids=[ asset_connection.asset.grid_model_id - for asset_connection in station.asset_connections - if asset_connection.asset.type in get_args(AssetBranchType) + for asset_connection in _combined_asset_connections(station) + if asset_connection.asset.asset_type in get_args(AssetBranchType) ], injection_ids=[ asset_connection.asset.grid_model_id - for asset_connection in station.asset_connections - if asset_connection.asset.type in get_args(AssetInjectionType) + for asset_connection in _combined_asset_connections(station) + if asset_connection.asset.asset_type in get_args(AssetInjectionType) ], close_couplers=True, ) diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py b/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py index 6ce268384..a6b978eed 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py @@ -16,7 +16,21 @@ from toop_engine_dc_solver.preprocess.preprocess_switching import ( make_optimal_separation_set, ) -from toop_engine_interfaces.asset_topology import Busbar, BusbarCoupler, MaterializedStation, SwitchableAsset +from toop_engine_interfaces.asset_topology import ( + Busbar, + BusbarCoupler, + MaterializedAssetConnection, + MaterializedStation, + SwitchableAsset, +) + + +def _asset_connections(*asset_ids: str) -> list[MaterializedAssetConnection]: + return [MaterializedAssetConnection(asset=SwitchableAsset(grid_model_id=asset_id)) for asset_id in asset_ids] + + +def _empty_injection_switching(n_busbars: int) -> np.ndarray: + return np.zeros((n_busbars, 0), dtype=bool) def test_realize_ba_to_physical_topo_per_station_simple(): @@ -24,15 +38,12 @@ def test_realize_ba_to_physical_topo_per_station_simple(): grid_model_id="teststation", busbars=[Busbar(grid_model_id="BB1", int_id=1), Busbar(grid_model_id="BB2", int_id=2)], couplers=[BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False)], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - ], - asset_connectivity=np.ones((2, 5), dtype=bool), - asset_switching_table=np.array([[True, True, False, False, False], [False, False, True, True, True]]), + branch_connections=_asset_connections("line1", "line2", "line3", "line4", "line5"), + injection_connections=[], + branch_connectivity=np.ones((2, 5), dtype=bool), + injection_connectivity=_empty_injection_switching(2), + branch_switching_table=np.array([[True, True, False, False, False], [False, False, True, True, True]]), + injection_switching_table=_empty_injection_switching(2), ) action_set = np.array( @@ -64,20 +75,20 @@ def test_realize_ba_to_physical_topo_per_station_simple(): # The second action should be without any reassignments assert realized_stations[1].couplers[0].open is True - assert np.array_equal(realized_stations[1].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[1].branch_switching_table, station.branch_switching_table) assert reassignment_distance[1] == 0 assert busbar_a_mappings[1] == [1] # The third action is again without reassignments but also without inversion assert realized_stations[2].couplers[0].open is True - assert np.array_equal(realized_stations[2].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[2].branch_switching_table, station.branch_switching_table) assert reassignment_distance[2] == 0 assert busbar_a_mappings[2] == [0] # The fourth action has one reassignment assert realized_stations[3].couplers[0].open is True expected_switching_table = np.array([[True, True, False, True, False], [False, False, True, False, True]]) - assert np.array_equal(realized_stations[3].asset_switching_table, expected_switching_table) + assert np.array_equal(realized_stations[3].branch_switching_table, expected_switching_table) assert reassignment_distance[3] == 2 assert busbar_a_mappings[3] == [0] @@ -92,14 +103,14 @@ def test_realize_ba_to_physical_topo_per_station_simple(): ) = compute_switching_table( local_action=jnp.array(action_set[1]), current_coupler_state=jnp.array([True]), - separation_set=jnp.array(station.asset_switching_table[None]), + separation_set=jnp.array(station.branch_switching_table[None]), coupler_states=jnp.array([[False]]), busbar_mapping=jnp.array([[False, True]]), - current_switching_table=jnp.array(station.asset_switching_table), - asset_connectivity=jnp.array(station.asset_connectivity), + current_switching_table=jnp.array(station.branch_switching_table), + asset_connectivity=jnp.array(station.branch_connectivity), choice_heuristic="first", ) - assert np.array_equal(current_switching_table, station.asset_switching_table) + assert np.array_equal(current_switching_table, station.branch_switching_table) assert np.array_equal(chosen_coupler_state, np.array([False])) assert np.array_equal(chosen_busbar_mapping, np.array([True, False])) # Inverted assert el_reassignment_distance == 0 @@ -133,17 +144,14 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False), BusbarCoupler(grid_model_id="BC2", busbar_from_id=2, busbar_to_id=3, open=False), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - ], - asset_connectivity=np.ones((3, 5), dtype=bool), - asset_switching_table=np.array( + branch_connections=_asset_connections("line1", "line2", "line3", "line4", "line5"), + injection_connections=[], + branch_connectivity=np.ones((3, 5), dtype=bool), + injection_connectivity=_empty_injection_switching(3), + branch_switching_table=np.array( [[True, True, False, False, False], [False, False, True, True, False], [False, False, False, False, True]] ), + injection_switching_table=_empty_injection_switching(3), ) action_set = np.array( @@ -178,14 +186,14 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): # The second action should be without any reassignments assert realized_stations[1].couplers[0].open is True assert realized_stations[1].couplers[1].open is False - assert np.array_equal(realized_stations[1].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[1].branch_switching_table, station.branch_switching_table) assert reassignment_distance[1] == 0 assert busbar_a_mappings[1] == [1, 2] # The third action is again without reassignments but also without inversion assert realized_stations[2].couplers[0].open is True assert realized_stations[2].couplers[1].open is False - assert np.array_equal(realized_stations[2].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[2].branch_switching_table, station.branch_switching_table) assert reassignment_distance[2] == 0 assert busbar_a_mappings[2] == [0] @@ -195,7 +203,7 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): expected_switching_table = np.array( [[True, True, False, False, True], [False, False, True, True, False], [False, False, False, False, False]] ) - assert np.array_equal(realized_stations[3].asset_switching_table, expected_switching_table) + assert np.array_equal(realized_stations[3].branch_switching_table, expected_switching_table) assert reassignment_distance[3] == 2 assert busbar_a_mappings[3] == [0] @@ -205,7 +213,7 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): expected_switching_table = np.array( [[True, False, False, True, False], [False, True, True, False, False], [False, False, False, False, True]] ) - assert np.array_equal(realized_stations[4].asset_switching_table, expected_switching_table) + assert np.array_equal(realized_stations[4].branch_switching_table, expected_switching_table) assert reassignment_distance[4] == 4 assert busbar_a_mappings[4] == [0] @@ -220,7 +228,7 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): expected_switching_table_least_connected = np.array( [[True, False, False, False, False], [False, False, True, True, False], [False, True, False, False, True]] ) - assert np.array_equal(realized_stations[5].asset_switching_table, expected_switching_table_first) + assert np.array_equal(realized_stations[5].branch_switching_table, expected_switching_table_first) assert reassignment_distance[5] == 2 assert busbar_a_mappings[5] == [0] @@ -237,7 +245,7 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): assert np.array_equal(updated_action_set_2, updated_action_set) assert realized_stations_2[5].couplers[0].open is True assert realized_stations_2[5].couplers[1].open is False - assert np.array_equal(realized_stations_2[5].asset_switching_table, expected_switching_table_least_connected) + assert np.array_equal(realized_stations_2[5].branch_switching_table, expected_switching_table_least_connected) assert reassignment_distance_2[5] == 2 assert busbar_a_mappings_2[5] == [0] @@ -255,7 +263,7 @@ def test_realize_ba_to_physical_topo_per_station_3_busbars(): assert np.array_equal(updated_action_set_3, updated_action_set) assert realized_stations_3[5].couplers[0].open is True assert realized_stations_3[5].couplers[1].open is False - assert np.array_equal(realized_stations_3[5].asset_switching_table, expected_switching_table_first) + assert np.array_equal(realized_stations_3[5].branch_switching_table, expected_switching_table_first) assert reassignment_distance_3[5] == 2 assert busbar_a_mappings_3[5] == [0] @@ -281,9 +289,12 @@ def test_realize_ba_to_physical_topo_per_station_large(): BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False), BusbarCoupler(grid_model_id="BC2", busbar_from_id=2, busbar_to_id=3, open=False), ], - assets=[SwitchableAsset(grid_model_id=f"line{i + 1}") for i in range(n_assets)], - asset_connectivity=np.ones((3, n_assets), dtype=bool), - asset_switching_table=switching_table, + branch_connections=_asset_connections(*[f"line{i + 1}" for i in range(n_assets)]), + injection_connections=[], + branch_connectivity=np.ones((3, n_assets), dtype=bool), + injection_connectivity=_empty_injection_switching(3), + branch_switching_table=switching_table, + injection_switching_table=_empty_injection_switching(3), ) separation_set_station = make_optimal_separation_set(station) @@ -316,19 +327,16 @@ def test_realize_ba_to_physical_topo_per_station_limited_connectivity(): BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False), BusbarCoupler(grid_model_id="BC2", busbar_from_id=2, busbar_to_id=3, open=False), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - ], - asset_connectivity=np.array( + branch_connections=_asset_connections("line1", "line2", "line3", "line4", "line5"), + injection_connections=[], + branch_connectivity=np.array( [[True, True, True, True, False], [True, True, True, True, False], [True, True, False, False, True]] ), - asset_switching_table=np.array( + injection_connectivity=_empty_injection_switching(3), + branch_switching_table=np.array( [[True, True, False, False, False], [False, False, True, True, False], [False, False, False, False, True]] ), + injection_switching_table=_empty_injection_switching(3), ) action_set = np.array( @@ -358,13 +366,13 @@ def test_realize_ba_to_physical_topo_per_station_limited_connectivity(): assert realized_stations[1].couplers[0].open is True assert realized_stations[1].couplers[1].open is False - assert np.array_equal(realized_stations[1].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[1].branch_switching_table, station.branch_switching_table) assert reassignment_distance[1] == 0 assert busbar_a_mappings[1] == [1, 2] assert realized_stations[2].couplers[0].open is True assert realized_stations[2].couplers[1].open is False - assert np.array_equal(realized_stations[2].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[2].branch_switching_table, station.branch_switching_table) assert reassignment_distance[2] == 0 assert busbar_a_mappings[2] == [0] @@ -372,24 +380,24 @@ def test_realize_ba_to_physical_topo_per_station_limited_connectivity(): expected_switching = np.array( [[False, False, True, True, False], [True, True, False, False, False], [False, False, False, False, True]] ) - assert np.array_equal(realized_stations[3].asset_switching_table, expected_switching) + assert np.array_equal(realized_stations[3].branch_switching_table, expected_switching) assert realized_stations[3].couplers[0].open is True assert realized_stations[3].couplers[1].open is False - assert reassignment_distance[3] == np.logical_xor(expected_switching, station.asset_switching_table).sum() + assert reassignment_distance[3] == np.logical_xor(expected_switching, station.branch_switching_table).sum() assert busbar_a_mappings[3] == [0] # Fourth action is the same as the fifth, but inverted - assert np.array_equal(realized_stations[4].asset_switching_table, expected_switching) + assert np.array_equal(realized_stations[4].branch_switching_table, expected_switching) assert realized_stations[4].couplers[0].open is True assert realized_stations[4].couplers[1].open is False - assert reassignment_distance[4] == np.logical_xor(expected_switching, station.asset_switching_table).sum() + assert reassignment_distance[4] == np.logical_xor(expected_switching, station.branch_switching_table).sum() assert busbar_a_mappings[4] == [1, 2] # Sixth action is possible by putting asset[3] on busbar[0] expected_switching = np.array( [[False, False, True, False, False], [True, True, False, True, False], [False, False, False, False, True]] ) - assert np.array_equal(realized_stations[5].asset_switching_table, expected_switching) + assert np.array_equal(realized_stations[5].branch_switching_table, expected_switching) assert realized_stations[5].couplers[0].open is True assert realized_stations[5].couplers[1].open is False @@ -406,19 +414,16 @@ def test_realize_ba_to_physical_topo_per_station_invalid_actions(): BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False), BusbarCoupler(grid_model_id="BC2", busbar_from_id=2, busbar_to_id=3, open=False), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - ], - asset_connectivity=np.array( + branch_connections=_asset_connections("line1", "line2", "line3", "line4", "line5"), + injection_connections=[], + branch_connectivity=np.array( [[True, True, False, False, True], [True, True, True, True, True], [True, True, False, False, True]] ), - asset_switching_table=np.array( + injection_connectivity=_empty_injection_switching(3), + branch_switching_table=np.array( [[True, True, False, False, False], [False, False, True, True, False], [False, False, False, False, True]] ), + injection_switching_table=_empty_injection_switching(3), ) separation_set_station = make_optimal_separation_set(station) @@ -462,19 +467,16 @@ def test_realize_ba_to_physical_topo_per_station_invalid_actions_hard(): BusbarCoupler(grid_model_id="BC1", busbar_from_id=1, busbar_to_id=2, open=False), BusbarCoupler(grid_model_id="BC2", busbar_from_id=2, busbar_to_id=3, open=False), ], - assets=[ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - SwitchableAsset(grid_model_id="line5"), - ], - asset_connectivity=np.array( + branch_connections=_asset_connections("line1", "line2", "line3", "line4", "line5"), + injection_connections=[], + branch_connectivity=np.array( [[True, True, False, False, False], [True, True, True, True, False], [True, True, False, False, True]] ), - asset_switching_table=np.array( + injection_connectivity=_empty_injection_switching(3), + branch_switching_table=np.array( [[True, True, False, False, False], [False, False, True, True, False], [False, False, False, False, True]] ), + injection_switching_table=_empty_injection_switching(3), ) action_set = np.array( @@ -505,6 +507,6 @@ def test_realize_ba_to_physical_topo_per_station_invalid_actions_hard(): assert realized_stations[1].couplers[0].open is True assert realized_stations[1].couplers[1].open is False - assert np.array_equal(realized_stations[1].asset_switching_table, station.asset_switching_table) + assert np.array_equal(realized_stations[1].branch_switching_table, station.branch_switching_table) assert reassignment_distance[1] == 0 assert busbar_a_mappings[1] == [1, 2] 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 2e6aee491..c723627b7 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 @@ -660,7 +660,7 @@ def create_busbar_b_in_ieee(net: pypowsybl.network.Network) -> None: def extract_station_info_powsybl(net: Network, base_folder: Path) -> None: - raw_stations, topology_assets = get_raw_stations_and_assets_bus_breaker(net) + raw_stations, branch_assets, injection_assets = get_raw_stations_and_assets_bus_breaker(net) target = base_folder / PREPROCESSING_PATHS["asset_topology_file_path"] target.parent.mkdir(parents=True, exist_ok=True) save_asset_topology( @@ -668,7 +668,8 @@ def extract_station_info_powsybl(net: Network, base_folder: Path) -> None: Topology( topology_id="extracted_topology", raw_stations=raw_stations, - assets=topology_assets, + branch_assets=branch_assets, + injection_assets=injection_assets, asset_bays=[], timestamp=datetime.datetime.now(), ), diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index 2a488496b..2a5a37a7f 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -16,20 +16,25 @@ import numpy as np import pandas as pd -from beartype.typing import Optional, Union +from beartype.typing import Optional, TypeVar, Union from jaxtyping import Bool from pypowsybl.network.impl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_helpers import change_dangling_to_tie, get_voltage_level_with_region from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, MaterializedStation, RawStation, StationAssetConnection, SwitchableAsset, Topology, + normalize_switchable_asset_payload, ) +SwitchableAssetType = TypeVar("SwitchableAssetType", bound=SwitchableAsset) + def get_all_element_names(network: Network, line_trafo_name_col: str = "elementName") -> pd.Series: """Get the names of all injections and branches in the network. @@ -125,46 +130,210 @@ def get_list_of_coupler_from_df(coupler_elements: pd.DataFrame) -> list[BusbarCo return coupler_list -def get_list_of_switchable_assets_from_df( - station_branches: pd.DataFrame, -) -> list[SwitchableAsset]: - """Get the list of switchable assets from the DataFrame. +def get_list_of_busbars_from_df(station_buses: pd.DataFrame) -> list[Busbar]: + """Get the list of busbars from the DataFrame. Parameters ---------- - station_branches: pd.DataFrame - DataFrame with the switchable assets + station_buses: pd.DataFrame + DataFrame with the busbars Note: datatype of columns is expected to be the same as in the pydantic model. Returns ------- - switchable_assets_list: list[SwitchableAsset] - List of switchable assets. + busbar_list: list[Busbar] + List of busbars. """ - switchable_assets_dict = station_branches.to_dict(orient="records") - switchable_assets_list = [SwitchableAsset(**switchable_asset) for switchable_asset in switchable_assets_dict] + busbar_dict = station_buses.to_dict(orient="records") + busbar_list = [Busbar(**busbar) for busbar in busbar_dict] - return switchable_assets_list + return busbar_list -def get_list_of_busbars_from_df(station_buses: pd.DataFrame) -> list[Busbar]: - """Get the list of busbars from the DataFrame. +def _dedupe_assets_by_id(assets: list[SwitchableAssetType]) -> list[SwitchableAssetType]: + """Deduplicate topology-owned assets by grid model id while preserving first-seen order. Parameters ---------- - station_buses: pd.DataFrame - DataFrame with the busbars - Note: datatype of columns is expected to be the same as in the pydantic model. + assets : list[SwitchableAssetType] + Topology-owned assets collected across stations. Returns ------- - busbar_list: list[Busbar] - List of busbars. + list[SwitchableAssetType] + One deep-copied asset per grid model id, ordered by first appearance. """ - busbar_dict = station_buses.to_dict(orient="records") - busbar_list = [Busbar(**busbar) for busbar in busbar_dict] + deduped_assets: dict[str, SwitchableAssetType] = {} + for asset in assets: + deduped_assets.setdefault(asset.grid_model_id, asset.model_copy(deep=True)) + return list(deduped_assets.values()) - return busbar_list + +def _get_station_asset_inputs_from_topology( + station_topology_elements: pd.DataFrame, + station_buses: pd.DataFrame, + dangling_lines: pd.DataFrame, + element_names: pd.Series, +) -> tuple[pd.DataFrame, list[str | None], np.ndarray, np.ndarray]: + """Build the shared station asset inputs used by the split extraction helpers. + + Parameters + ---------- + station_topology_elements : pd.DataFrame + Raw powsybl topology elements for one station. + station_buses : pd.DataFrame + Formatted station busbars with local integer ids. + dangling_lines : pd.DataFrame + Network dangling-line metadata used to normalize tie lines. + element_names : pd.Series + Asset names keyed by grid model id. + + Returns + ------- + station_elements : pd.DataFrame + Normalized station asset rows used for asset materialization. + asset_terminals : list[str | None] + Terminal labels aligned with `station_elements`. + switching_matrix : np.ndarray + Station-local switching matrix aligned with `station_elements`. + asset_connectivity : np.ndarray + Default connectivity matrix aligned with `station_elements`. + """ + station_elements, switching_matrix = get_asset_info_from_topology( + station_topology_elements, + station_buses, + dangling_lines, + element_names, + ) + asset_connectivity = np.ones_like(switching_matrix, dtype=bool) + asset_terminals = ( + station_elements["branch_end"].tolist() + if "branch_end" in station_elements.columns + else [None] * len(station_elements) + ) + return station_elements, asset_terminals, switching_matrix, asset_connectivity + + +def _get_branch_station_assets_from_df( + station_elements: pd.DataFrame, + asset_terminals: list[str | None], + switching_matrix: np.ndarray, + asset_connectivity: np.ndarray, +) -> tuple[list[BranchAsset], list[str | None], np.ndarray, np.ndarray]: + """Return branch station assets and aligned arrays from prepared station inputs. + + Parameters + ---------- + station_elements : pd.DataFrame + Normalized station asset rows. + asset_terminals : list[str | None] + Terminal labels aligned with `station_elements`. + switching_matrix : np.ndarray + Station-local switching matrix aligned with `station_elements`. + asset_connectivity : np.ndarray + Connectivity matrix aligned with `station_elements`. + + Returns + ------- + branch_assets : list[BranchAsset] + Materialized branch assets for the station. + branch_terminals : list[str | None] + Terminal labels aligned with `branch_assets`. + branch_switching_table : np.ndarray + Branch-only switching table. + branch_connectivity : np.ndarray + Branch-only connectivity table. + """ + normalized_assets = [ + normalize_switchable_asset_payload(switchable_asset) + for switchable_asset in station_elements.to_dict(orient="records") + ] + branch_mask = np.asarray([asset.is_branch() is not False for asset in normalized_assets], dtype=bool) + branch_assets = [ + asset if isinstance(asset, BranchAsset) else BranchAsset.model_validate(asset.model_dump()) + for asset, is_branch in zip(normalized_assets, branch_mask, strict=True) + if is_branch + ] + branch_terminals = [terminal for terminal, is_branch in zip(asset_terminals, branch_mask, strict=True) if is_branch] + branch_switching_table = switching_matrix[:, branch_mask] + branch_connectivity = asset_connectivity[:, branch_mask] + + return branch_assets, branch_terminals, branch_switching_table, branch_connectivity + + +def _get_injection_station_assets_from_df( + station_elements: pd.DataFrame, + asset_terminals: list[str | None], + switching_matrix: np.ndarray, + asset_connectivity: np.ndarray, +) -> tuple[list[InjectionAsset], list[str | None], np.ndarray, np.ndarray]: + """Return injection station assets and aligned arrays from prepared station inputs. + + Parameters + ---------- + station_elements : pd.DataFrame + Normalized station asset rows. + asset_terminals : list[str | None] + Terminal labels aligned with `station_elements`. + switching_matrix : np.ndarray + Station-local switching matrix aligned with `station_elements`. + asset_connectivity : np.ndarray + Connectivity matrix aligned with `station_elements`. + + Returns + ------- + injection_assets : list[InjectionAsset] + Materialized injection assets for the station. + injection_terminals : list[str | None] + Terminal labels aligned with `injection_assets`. + injection_switching_table : np.ndarray + Injection-only switching table. + injection_connectivity : np.ndarray + Injection-only connectivity table. + """ + normalized_assets = [ + normalize_switchable_asset_payload(switchable_asset) + for switchable_asset in station_elements.to_dict(orient="records") + ] + injection_mask = np.asarray([asset.is_branch() is False for asset in normalized_assets], dtype=bool) + injection_assets = [ + asset if isinstance(asset, InjectionAsset) else InjectionAsset.model_validate(asset.model_dump()) + for asset, is_injection in zip(normalized_assets, injection_mask, strict=True) + if is_injection + ] + injection_terminals = [ + terminal for terminal, is_injection in zip(asset_terminals, injection_mask, strict=True) if is_injection + ] + injection_switching_table = switching_matrix[:, injection_mask] + injection_connectivity = asset_connectivity[:, injection_mask] + + return injection_assets, injection_terminals, injection_switching_table, injection_connectivity + + +def _get_single_topology_kind(buses_with_substation_and_voltage: pd.DataFrame) -> str: + """Return the shared topology kind for all relevant stations or raise on mixed input. + + Parameters + ---------- + buses_with_substation_and_voltage : pd.DataFrame + Relevant station rows including the `topology_kind` column. + + Returns + ------- + str + The single topology kind shared by all relevant stations. + + Raises + ------ + ValueError + If the relevant stations contain a mix of topology kinds. + """ + topology_kinds = buses_with_substation_and_voltage["topology_kind"].unique() + if len(topology_kinds) != 1: + raise ValueError( + "Relevant stations must be either of kind NODE_BREAKER or BUS_BREAKER, a mix of both is not permitted" + ) + return str(topology_kinds[0]) def get_bus_info_from_topology(station_buses: pd.DataFrame, bus_id: str) -> pd.DataFrame: @@ -226,7 +395,7 @@ def get_coupler_info_from_topology( # rename the columns to match the pydantic model coupler_elements.rename( columns={ - "kind": "type", + "kind": "coupler_type", "bus1_id": "busbar_from_id", "bus2_id": "busbar_to_id", "id": "grid_model_id", @@ -323,7 +492,8 @@ def get_asset_info_from_topology( station_elements = station_elements[station_elements["bus_id"].isin(station_buses["grid_model_id"])] switching_matrix = get_asset_switching_table(station_buses=station_buses, station_elements=station_elements) # get columns for pydantic model - station_elements = station_elements[["grid_model_id", "type", "name", "in_service"]].reset_index(drop=True) + station_elements = station_elements.rename(columns={"type": "asset_type"}) + station_elements = station_elements[["grid_model_id", "asset_type", "name", "in_service"]].reset_index(drop=True) return station_elements, switching_matrix @@ -354,14 +524,15 @@ def get_relevant_network_data( relevant_buses = network.get_buses(attributes=["voltage_level_id"]).loc[relevant_stations] voltage_level_df = get_voltage_level_with_region(network, attributes=["substation_id", "nominal_v", "topology_kind"]) buses_with_substation_and_voltage = relevant_buses.merge(voltage_level_df, left_on="voltage_level_id", right_index=True) - if buses_with_substation_and_voltage["topology_kind"].unique() == "BUS_BREAKER": + topology_kind = _get_single_topology_kind(buses_with_substation_and_voltage) + if topology_kind == "BUS_BREAKER": if "elementName" in network.get_lines(all_attributes=True).columns: # For UCTE models, the name is stored in elementName element_name_col = "elementName" else: # All other grid files use normally "name" element_name_col = "name" - elif buses_with_substation_and_voltage["topology_kind"].unique() == "NODE_BREAKER": + elif topology_kind == "NODE_BREAKER": element_name_col = "name" else: raise ValueError( @@ -400,7 +571,7 @@ def get_relevant_stations( ) # Calculate the pydantic station for each relevant bus - raw_stations, _ = get_raw_stations_and_assets( + raw_stations, _, _ = get_raw_stations_and_assets( network, buses_with_substation_and_voltage, switches, dangling_lines, element_names ) return raw_stations @@ -412,7 +583,7 @@ def get_raw_stations_and_assets( switches: pd.DataFrame, dangling_lines: pd.DataFrame, element_names: pd.Series, -) -> tuple[list[RawStation], list[SwitchableAsset]]: +) -> tuple[list[RawStation], list[BranchAsset], list[InjectionAsset]]: """Build raw topology stations and topology-owned assets from the relevant buses. Parameters @@ -432,27 +603,48 @@ def get_raw_stations_and_assets( ------- station_list : list[RawStation] List of all lean topology stations of the relevant buses in the network - topology_assets : list[SwitchableAsset] - Deduplicated topology-owned assets referenced by the stations + branch_assets : list[BranchAsset] + Deduplicated topology-owned branch assets referenced by the stations + injection_assets : list[InjectionAsset] + Deduplicated topology-owned injection assets referenced by the stations """ station_list: list[RawStation] = [] - topology_assets: dict[str, SwitchableAsset] = {} + all_branch_assets: list[BranchAsset] = [] + all_injection_assets: list[InjectionAsset] = [] for bus_id, bus_info in buses_with_substation_and_voltage.iterrows(): station_topology = network.get_bus_breaker_topology(bus_info.voltage_level_id) station_buses = get_bus_info_from_topology(station_topology.buses, bus_id) coupler_elements = get_coupler_info_from_topology(station_topology.switches, switches, station_buses) - station_elements, switching_matrix = get_asset_info_from_topology( - station_topology.elements, station_buses, dangling_lines, element_names + station_elements, asset_terminals, switching_matrix, asset_connectivity = _get_station_asset_inputs_from_topology( + station_topology.elements, + station_buses, + dangling_lines, + element_names, ) - asset_connectivity = np.ones_like(switching_matrix, dtype=bool) - asset_terminals = ( - station_elements["branch_end"].tolist() - if "branch_end" in station_elements.columns - else [None] * len(station_elements) + ( + branch_assets, + branch_terminals, + branch_switching_table, + branch_connectivity, + ) = _get_branch_station_assets_from_df( + station_elements, + asset_terminals, + switching_matrix, + asset_connectivity, ) - assets = get_list_of_switchable_assets_from_df(station_elements) - for asset in assets: - topology_assets.setdefault(asset.grid_model_id, asset.model_copy(deep=True)) + ( + injection_assets, + injection_terminals, + injection_switching_table, + injection_connectivity, + ) = _get_injection_station_assets_from_df( + station_elements, + asset_terminals, + switching_matrix, + asset_connectivity, + ) + all_branch_assets.extend(branch_assets) + all_injection_assets.extend(injection_assets) station = RawStation( grid_model_id=bus_id, @@ -461,15 +653,25 @@ def get_raw_stations_and_assets( voltage_level=bus_info.nominal_v, busbars=get_list_of_busbars_from_df(station_buses), couplers=get_list_of_coupler_from_df(coupler_elements), - asset_connections=[ + branch_connections=[ + StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) + for asset, asset_terminal in zip(branch_assets, branch_terminals, strict=True) + ], + injection_connections=[ StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) - for asset, asset_terminal in zip(assets, asset_terminals, strict=True) + for asset, asset_terminal in zip(injection_assets, injection_terminals, strict=True) ], - asset_switching_table=switching_matrix, - asset_connectivity=asset_connectivity, + branch_switching_table=branch_switching_table, + injection_switching_table=injection_switching_table, + branch_connectivity=branch_connectivity, + injection_connectivity=injection_connectivity, ) station_list.append(station) - return station_list, list(topology_assets.values()) + return ( + station_list, + _dedupe_assets_by_id(all_branch_assets), + _dedupe_assets_by_id(all_injection_assets), + ) def get_topology( @@ -501,7 +703,7 @@ def get_topology( network=network, relevant_stations=relevant_stations, ) - raw_stations, topology_assets = get_raw_stations_and_assets( + raw_stations, branch_assets, injection_assets = get_raw_stations_and_assets( network, buses_with_substation_and_voltage, switches, dangling_lines, element_names ) timestamp = datetime.datetime.now() @@ -510,13 +712,16 @@ def get_topology( topology_id=topology_id, grid_model_file=grid_model_file, raw_stations=raw_stations, - assets=topology_assets, + branch_assets=branch_assets, + injection_assets=injection_assets, asset_bays=[], timestamp=timestamp, ) -def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStation], list[SwitchableAsset]]: +def get_raw_stations_and_assets_bus_breaker( + net: Network, +) -> tuple[list[RawStation], list[BranchAsset], list[InjectionAsset]]: """Convert a bus-breaker topology grid to raw topology stations and topology-owned assets. This is mainly used for fixture and test-grid extraction. @@ -530,8 +735,10 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati ------- stations : list[RawStation] List of all lean topology stations in the network - topology_assets : list[SwitchableAsset] - Deduplicated topology-owned assets referenced by the raw stations + branch_assets : list[BranchAsset] + Deduplicated topology-owned branch assets referenced by the raw stations + injection_assets : list[InjectionAsset] + Deduplicated topology-owned injection assets referenced by the raw stations """ all_switches = net.get_switches(all_attributes=True) all_branches = net.get_branches(all_attributes=True) @@ -539,7 +746,8 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati all_breaker_buses = net.get_bus_breaker_view_buses(all_attributes=True) stations: list[RawStation] = [] - topology_assets: dict[str, SwitchableAsset] = {} + all_branch_assets: list[BranchAsset] = [] + all_injection_assets: list[InjectionAsset] = [] for bus_id, bus_row in net.get_buses().iterrows(): local_buses = all_breaker_buses[all_breaker_buses["bus_id"] == bus_id] local_switches = all_switches[ @@ -565,6 +773,7 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati couplers = [ BusbarCoupler( grid_model_id=grid_model_id, + coupler_type=switch.kind, busbar_from_id=busbar_mapper[switch.bus_breaker_bus1_id], busbar_to_id=busbar_mapper[switch.bus_breaker_bus2_id], open=switch.open, @@ -572,47 +781,54 @@ def get_raw_stations_and_assets_bus_breaker(net: Network) -> tuple[list[RawStati for grid_model_id, switch in local_switches.iterrows() ] from_branch_assets = [ - SwitchableAsset(grid_model_id=grid_model_id, type=branch.type) + BranchAsset(grid_model_id=grid_model_id, asset_type=branch.type) for grid_model_id, branch in from_branches.iterrows() ] to_branch_assets = [ - SwitchableAsset(grid_model_id=grid_model_id, type=branch.type) + BranchAsset(grid_model_id=grid_model_id, asset_type=branch.type) for grid_model_id, branch in to_branches.iterrows() ] injection_assets = [ - SwitchableAsset(grid_model_id=grid_model_id, type=injection.type) + InjectionAsset(grid_model_id=grid_model_id, asset_type=injection.type) for grid_model_id, injection in injections.iterrows() ] - assets = from_branch_assets + to_branch_assets + injection_assets - asset_terminals = ( - ["from"] * len(from_branch_assets) + ["to"] * len(to_branch_assets) + [None] * len(injection_assets) - ) - from_branch_bus_index = [busbar_mapper[branch.bus_breaker_bus1_id] for branch in from_branches.itertuples()] - to_branch_bus_index = [busbar_mapper[branch.bus_breaker_bus2_id] for branch in to_branches.itertuples()] + branch_assets = from_branch_assets + to_branch_assets + branch_terminals = ["from"] * len(from_branch_assets) + ["to"] * len(to_branch_assets) + branch_bus_index = [busbar_mapper[branch.bus_breaker_bus1_id] for branch in from_branches.itertuples()] + [ + busbar_mapper[branch.bus_breaker_bus2_id] for branch in to_branches.itertuples() + ] injection_bus_index = [busbar_mapper[injection.bus_breaker_bus_id] for injection in injections.itertuples()] - bus_index = from_branch_bus_index + to_branch_bus_index + injection_bus_index - switching_table = np.zeros((len(busbars), len(assets)), dtype=bool) - for asset_index, idx in enumerate(bus_index): - switching_table[idx, asset_index] = True + branch_switching_table = np.zeros((len(busbars), len(branch_assets)), dtype=bool) + for asset_index, idx in enumerate(branch_bus_index): + branch_switching_table[idx, asset_index] = True - for asset in assets: - topology_assets.setdefault(asset.grid_model_id, asset.model_copy(deep=True)) + injection_switching_table = np.zeros((len(busbars), len(injection_assets)), dtype=bool) + for asset_index, idx in enumerate(injection_bus_index): + injection_switching_table[idx, asset_index] = True + + all_branch_assets.extend(branch_assets) + all_injection_assets.extend(injection_assets) station = RawStation( grid_model_id=bus_id, name=bus_row.name, busbars=busbars, couplers=couplers, - asset_connections=[ + branch_connections=[ StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) - for asset, asset_terminal in zip(assets, asset_terminals, strict=True) + for asset, asset_terminal in zip(branch_assets, branch_terminals, strict=True) + ], + injection_connections=[ + StationAssetConnection(asset_id=asset.grid_model_id, terminal=None, asset_bay_id=None) + for asset in injection_assets ], - asset_switching_table=switching_table, + branch_switching_table=branch_switching_table, + injection_switching_table=injection_switching_table, ) stations.append(station) - return stations, list(topology_assets.values()) + return stations, _dedupe_assets_by_id(all_branch_assets), _dedupe_assets_by_id(all_injection_assets) # TODO: refactor due to C901 @@ -654,13 +870,14 @@ def assert_station_in_network( # noqa: C901 raise ValueError(f"Station {station.grid_model_id} not found in the network") bus_breaker_topo = net.get_bus_breaker_topology(buses_df.loc[station.grid_model_id]["voltage_level_id"]) + station_connections = [*station.branch_connections, *station.injection_connections] - for asset_connection in station.asset_connections: + for asset_connection in station_connections: asset = asset_connection.asset if asset.grid_model_id not in bus_breaker_topo.elements.index: raise ValueError(f"Asset {asset.grid_model_id} not found in the station elements: {bus_breaker_topo.elements}") - if assets_strict and len(bus_breaker_topo.elements) != len(station.asset_connections): - raise ValueError(f"Asset count mismatch: {len(bus_breaker_topo.elements)} != {len(station.asset_connections)}") + if assets_strict and len(bus_breaker_topo.elements) != len(station_connections): + raise ValueError(f"Asset count mismatch: {len(bus_breaker_topo.elements)} != {len(station_connections)}") for busbar in station.busbars: if busbar.grid_model_id not in bus_breaker_topo.buses.index: diff --git a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py index 5025541af..4c95057a7 100644 --- a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py @@ -13,6 +13,8 @@ import pytest from toop_engine_grid_helpers.powsybl.example_grids import basic_node_breaker_network_powsybl, create_busbar_b_in_ieee from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import ( + _get_branch_station_assets_from_df, + _get_injection_station_assets_from_df, assert_station_in_network, get_all_element_names, get_asset_info_from_topology, @@ -21,7 +23,6 @@ get_coupler_info_from_topology, get_list_of_busbars_from_df, get_list_of_coupler_from_df, - get_list_of_switchable_assets_from_df, get_name_of_station_elements, get_raw_stations_and_assets_bus_breaker, get_relevant_network_data, @@ -29,10 +30,11 @@ get_topology, ) from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, RawStation, - SwitchableAsset, Topology, ) from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS @@ -87,7 +89,7 @@ def test_get_list_of_coupler_from_df(): data={ "grid_model_id": ["coupler1", "coupler2"], "bus_int_id": [1, 2], - "type": ["type1", "type2"], + "coupler_type": ["type1", "type2"], "name": ["name1", "name2"], "busbar_from_id": [1, 2], "busbar_to_id": [2, 3], @@ -98,7 +100,7 @@ def test_get_list_of_coupler_from_df(): expected_coupler_list = [ BusbarCoupler( grid_model_id="coupler1", - type="type1", + coupler_type="type1", name="name1", busbar_from_id=1, busbar_to_id=2, @@ -107,7 +109,7 @@ def test_get_list_of_coupler_from_df(): ), BusbarCoupler( grid_model_id="coupler2", - type="type2", + coupler_type="type2", name="name2", busbar_from_id=2, busbar_to_id=3, @@ -119,34 +121,71 @@ def test_get_list_of_coupler_from_df(): assert coupler_list == expected_coupler_list -def test_get_list_of_switchable_assets_from_df(): +def test_get_branch_and_injection_station_assets_from_df(monkeypatch: pytest.MonkeyPatch): asset_elements = pd.DataFrame( data={ - "grid_model_id": ["asset1", "asset2"], - "bus_int_id": [1, 2], - "type": ["TIE_LINE", "LINE"], - "name": ["name1", "name2"], - "in_service": [True, True], + "grid_model_id": ["asset1", "asset2", "asset3"], + "asset_type": ["TIE_LINE", "LINE", "LOAD"], + "name": ["name1", "name2", "name3"], + "in_service": [True, True, True], } ) - expected_asset_list = [ - SwitchableAsset( + asset_terminals = ["from", "to", None] + switching_matrix = np.array( + [ + [True, False, False], + [False, True, True], + ], + dtype=bool, + ) + asset_connectivity = np.ones_like(switching_matrix, dtype=bool) + + expected_branch_assets = [ + BranchAsset( grid_model_id="asset1", - type="TIE_LINE", + asset_type="TIE_LINE", name="name1", - bus_int_id=1, in_service=True, ), - SwitchableAsset( + BranchAsset( grid_model_id="asset2", - type="LINE", + asset_type="LINE", name="name2", - bus_int_id=2, in_service=True, ), ] - asset_list = get_list_of_switchable_assets_from_df(asset_elements) - assert asset_list == expected_asset_list + expected_injection_assets = [ + InjectionAsset( + grid_model_id="asset3", + asset_type="LOAD", + name="name3", + in_service=True, + ) + ] + + branch_assets, branch_terminals, branch_switching_table, branch_connectivity = _get_branch_station_assets_from_df( + asset_elements, + asset_terminals, + switching_matrix, + asset_connectivity, + ) + injection_assets, injection_terminals, injection_switching_table, injection_connectivity = ( + _get_injection_station_assets_from_df( + asset_elements, + asset_terminals, + switching_matrix, + asset_connectivity, + ) + ) + + assert branch_assets == expected_branch_assets + assert injection_assets == expected_injection_assets + assert branch_terminals == ["from", "to"] + assert injection_terminals == [None] + assert np.array_equal(branch_switching_table, switching_matrix[:, :2]) + assert np.array_equal(injection_switching_table, switching_matrix[:, 2:]) + assert np.array_equal(branch_connectivity, asset_connectivity[:, :2]) + assert np.array_equal(injection_connectivity, asset_connectivity[:, 2:]) def test_get_list_of_busbars_from_df(): @@ -154,7 +193,7 @@ def test_get_list_of_busbars_from_df(): data={ "grid_model_id": ["busbar1", "busbar2"], "int_id": [1, 2], - "type": ["type1", "type2"], + "busbar_type": ["type1", "type2"], "name": ["name1", "name2"], "in_service": [True, True], } @@ -163,14 +202,14 @@ def test_get_list_of_busbars_from_df(): Busbar( grid_model_id="busbar1", int_id=1, - type="type1", + busbar_type="type1", name="name1", in_service=True, ), Busbar( grid_model_id="busbar2", int_id=2, - type="type2", + busbar_type="type2", name="name2", in_service=True, ), @@ -234,7 +273,7 @@ def test_get_asset_info_from_topology(): index=[0, 1], data={ "grid_model_id": ["line1", "tie_line1"], - "type": ["LINE", "TIE_LINE"], + "asset_type": ["LINE", "TIE_LINE"], "name": ["line_name", "tie"], "in_service": [True, True], }, @@ -276,7 +315,7 @@ def test_get_coupler_info_from_topology(): "busbar_from_id": [0, 0], "busbar_to_id": [1, 1], "open": [True, True], - "type": ["BREAKER", "DISCONNECTOR"], + "coupler_type": ["BREAKER", "DISCONNECTOR"], "name": ["break_1", "disco_1"], "in_service": [True, True], }, @@ -437,19 +476,27 @@ def test_assert_station_in_network_coupler(case14_data_with_asset_topo: tuple[Pa def test_assert_station_in_network_busbar(case14_data_with_asset_topo: tuple[Path, Topology]) -> None: grid_path, topology = case14_data_with_asset_topo net = pypowsybl.network.load(grid_path / PREPROCESSING_PATHS["grid_file_path_powsybl"]) + base_station = topology.materialize_stations()[0] # Add a busbar to the station that is not in the grid - station = topology.materialize_stations()[0].model_copy( + station = base_station.model_copy( update={ "busbars": [ - topology.materialize_stations()[0].busbars[0], - topology.materialize_stations()[0].busbars[1], - topology.materialize_stations()[0].busbars[0].model_copy(update={"grid_model_id": "hugawuga", "int_id": 3}), + base_station.busbars[0], + base_station.busbars[1], + base_station.busbars[0].model_copy(update={"grid_model_id": "hugawuga", "int_id": 3}), ], - "asset_switching_table": np.concatenate( + "branch_switching_table": np.concatenate( + [ + base_station.branch_switching_table, + base_station.branch_switching_table[0:1], + ], + axis=0, + ), + "injection_switching_table": np.concatenate( [ - topology.materialize_stations()[0].asset_switching_table, - topology.materialize_stations()[0].asset_switching_table[0:1], + base_station.injection_switching_table, + base_station.injection_switching_table[0:1], ], axis=0, ), @@ -459,13 +506,21 @@ def test_assert_station_in_network_busbar(case14_data_with_asset_topo: tuple[Pat assert_station_in_network(net, station) # Remove a busbar from the station - station = topology.materialize_stations()[0].model_copy( - update={ - "busbars": [ - topology.materialize_stations()[0].busbars[0], - ], - "asset_switching_table": topology.materialize_stations()[0].asset_switching_table[0:1], - } + station = type(base_station).model_construct( + grid_model_id=base_station.grid_model_id, + name=base_station.name, + station_type=base_station.station_type, + region=base_station.region, + voltage_level=base_station.voltage_level, + busbars=[base_station.busbars[0]], + couplers=base_station.couplers, + branch_connections=base_station.branch_connections, + injection_connections=base_station.injection_connections, + branch_switching_table=base_station.branch_switching_table[0:1], + injection_switching_table=base_station.injection_switching_table[0:1], + branch_connectivity=None, + injection_connectivity=None, + model_log=base_station.model_log, ) # Should pass without strict assert_station_in_network(net, station, busbars_strict=False) @@ -479,35 +534,68 @@ def test_assert_station_in_network_asset(case14_data_with_asset_topo: tuple[Path # Add a switchable asset to the station that is not in the grid base_station = topology.materialize_stations()[0] - station = base_station.model_copy( - update={ - "asset_connections": base_station.asset_connections - + [ - base_station.asset_connections[0].model_copy( - update={ - "asset": base_station.asset_connections[0].asset.model_copy(update={"grid_model_id": "hugawuga"}) - } + if base_station.branch_connections: + station = base_station.model_copy( + update={ + "branch_connections": base_station.branch_connections + + [ + base_station.branch_connections[0].model_copy( + update={ + "asset": base_station.branch_connections[0].asset.model_copy( + update={"grid_model_id": "hugawuga"} + ) + } + ), + ], + "branch_switching_table": np.concatenate( + [ + base_station.branch_switching_table, + base_station.branch_switching_table[:, 0:1], + ], + axis=1, ), - ], - "asset_switching_table": np.concatenate( - [ - base_station.asset_switching_table, - base_station.asset_switching_table[:, 0:1], + } + ) + else: + station = base_station.model_copy( + update={ + "injection_connections": base_station.injection_connections + + [ + base_station.injection_connections[0].model_copy( + update={ + "asset": base_station.injection_connections[0].asset.model_copy( + update={"grid_model_id": "hugawuga"} + ) + } + ), ], - axis=1, - ), - } - ) + "injection_switching_table": np.concatenate( + [ + base_station.injection_switching_table, + base_station.injection_switching_table[:, 0:1], + ], + axis=1, + ), + } + ) with pytest.raises(ValueError, match="Asset hugawuga not found in the station elements"): assert_station_in_network(net, station) # Remove a switchable asset from the station - station = base_station.model_copy( - update={ - "asset_connections": base_station.asset_connections[:-1], - "asset_switching_table": base_station.asset_switching_table[:, :-1], - } - ) + if base_station.branch_connections: + station = base_station.model_copy( + update={ + "branch_connections": base_station.branch_connections[:-1], + "branch_switching_table": base_station.branch_switching_table[:, :-1], + } + ) + else: + station = base_station.model_copy( + update={ + "injection_connections": base_station.injection_connections[:-1], + "injection_switching_table": base_station.injection_switching_table[:, :-1], + } + ) # Should pass without strict assert_station_in_network(net, station, assets_strict=False) with pytest.raises(ValueError, match="Asset count mismatch"): @@ -518,17 +606,20 @@ def test_convert_bus_breaker_stations_to_asset_topo() -> None: net = pypowsybl.network.create_ieee30() create_busbar_b_in_ieee(net) - stations, assets = get_raw_stations_and_assets_bus_breaker(net) + stations, branch_assets, injection_assets = get_raw_stations_and_assets_bus_breaker(net) + assets = [*branch_assets, *injection_assets] assert len(stations) == 30 for station in stations: assert len(station.busbars) == 2 assert len(station.couplers) == 1 - for asset_id in [asset_connection.asset_id for asset_connection in station.asset_connections]: + for asset_id in [ + asset_connection.asset_id for asset_connection in [*station.branch_connections, *station.injection_connections] + ]: assert any(asset.grid_model_id == asset_id for asset in assets) - for asset in assets: - if asset.is_branch(): - assert asset.grid_model_id in net.get_branches().index - else: - assert asset.grid_model_id in net.get_injections().index + for asset in branch_assets: + assert asset.grid_model_id in net.get_branches().index + + for asset in injection_assets: + assert asset.grid_model_id in net.get_injections().index diff --git a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py index 030607d81..1e417abcf 100644 --- a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py +++ b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py @@ -17,6 +17,7 @@ from pathlib import Path +import numpy as np import pandas as pd import structlog from beartype.typing import Optional, Union @@ -295,14 +296,17 @@ def get_changes_from_switching_table( List of tuples with asset name, initial_busbar and final_busbar Note: initial_busbar and final_busbar can both be None if asset is disconnected """ - switching_table = station.asset_switching_table + switching_table = np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1) busbar_name_list = [busbar.grid_model_id for busbar in station.busbars] - asset_list = [asset_connection.asset for asset_connection in station.asset_connections] + asset_list = [ + *(asset_connection.asset for asset_connection in station.branch_connections), + *(asset_connection.asset for asset_connection in station.injection_connections), + ] change_list = [] # TODO: make a dataclass for this (code style) # loop over assets -> by column for asset_index, asset_in_table in enumerate(switching_table.T): asset_name = asset_list[asset_index].grid_model_id - asset_type = asset_list[asset_index].type + asset_type = asset_list[asset_index].asset_type if asset_in_table.sum() > 1: raise ValueError( f"Asset {asset_list[asset_index].grid_model_id} is connected to multiple" diff --git a/packages/importer_pkg/src/toop_engine_importer/network_graph/data_classes.py b/packages/importer_pkg/src/toop_engine_importer/network_graph/data_classes.py index abf2cab4f..d1b85a289 100644 --- a/packages/importer_pkg/src/toop_engine_importer/network_graph/data_classes.py +++ b/packages/importer_pkg/src/toop_engine_importer/network_graph/data_classes.py @@ -372,7 +372,7 @@ class SwitchableAssetSchema(pa.DataFrameModel): name: pat.Series[str] """The name of the asset.""" - type: pat.Series[str] + asset_type: pat.Series[str] """The type of the asset, e.g. LINE, TWO_WINDING_TRANSFORMER, etc.""" in_service: pat.Series[bool] 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 c54f6360e..c2cfc8d46 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 @@ -53,11 +53,11 @@ def get_busbar_df(nodes_df: pat.DataFrame[NodeSchema], substation_id: str) -> pd busbar_df = ( busbar_df.sort_values(by="grid_model_id") .reset_index() - .rename(columns={"foreign_id": "name", "node_type": "type", "bus_id": "bus_branch_bus_id"}) + .rename(columns={"foreign_id": "name", "node_type": "busbar_type", "bus_id": "bus_branch_bus_id"}) ) busbar_df["int_id"] = busbar_df.index - busbar_df = busbar_df[["grid_model_id", "type", "name", "int_id", "in_service", "bus_branch_bus_id"]] + busbar_df = busbar_df[["grid_model_id", "busbar_type", "name", "int_id", "in_service", "bus_branch_bus_id"]] return busbar_df @@ -104,7 +104,7 @@ def get_coupler_df( coupler_df = switches_connected_to_busbars[switches_connected_to_busbars["coupler_type"] != ""] coupler_df["from_busbar_grid_model_id"] = "" coupler_df["to_busbar_grid_model_id"] = "" - coupler_df["type"] = "" + coupler_df["coupler_type"] = "" # hotfix in case a bay id has not been identified for a switch # a missing bay id indecates there is a data quality issue @@ -139,7 +139,7 @@ def get_coupler_df( out_of_service_busbar_ids=busbar_out_of_service, ignore_busbar_id=coupler_df.loc[index, "from_busbar_grid_model_id"], ) - coupler_df.loc[index, "type"] = row["asset_type"] + coupler_df.loc[index, "coupler_type"] = row["asset_type"] bay_state = get_state_of_coupler_based_on_bay( coupler_index=index, bay_df=bay_df, @@ -168,7 +168,9 @@ def get_coupler_df( suffixes=("", "_to"), ) coupler_df.rename(columns={"int_id": "busbar_to_id"}, inplace=True) - coupler_df = coupler_df[["grid_model_id", "type", "name", "in_service", "open", "busbar_from_id", "busbar_to_id"]] + coupler_df = coupler_df[ + ["grid_model_id", "coupler_type", "name", "in_service", "open", "busbar_from_id", "busbar_to_id"] + ] return coupler_df @@ -346,8 +348,8 @@ def get_switchable_asset( connected_asset_df["in_service"].notna(), connected_asset_df["in_service"], connected_asset_df["in_service_1"] ) # rename columns to match the AssetTopology - connected_asset_df.rename(columns={"asset_type": "type", "foreign_id": "name"}, inplace=True) - connected_asset_df = connected_asset_df[["grid_model_id", "name", "type", "in_service"]] + connected_asset_df.rename(columns={"foreign_id": "name"}, inplace=True) + connected_asset_df = connected_asset_df[["grid_model_id", "name", "asset_type", "in_service"]] # ensure the order of the assets connected_asset_df.sort_values(by="grid_model_id", inplace=True) connected_asset_df.reset_index(drop=True, inplace=True) 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 6bd2c76a2..8f75ba717 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 @@ -19,7 +19,6 @@ get_all_element_names, get_list_of_busbars_from_df, get_list_of_coupler_from_df, - get_list_of_switchable_assets_from_df, ) from toop_engine_importer.network_graph.data_classes import ( HelperBranchSchema, @@ -53,9 +52,11 @@ ) from toop_engine_importer.pypowsybl_import.powsybl_masks import NetworkMasks from toop_engine_interfaces.asset_topology import ( + AssetBay, MaterializedAssetConnection, MaterializedStation, Topology, + normalize_switchable_asset_payload, topology_parts_from_materialized_station, ) from toop_engine_interfaces.messages.preprocess.preprocess_commands import CgmesImporterParameters @@ -98,6 +99,153 @@ def _expand_busbars_connected_via_switches( return selected_busbar_ids +def _get_station_busbar_view( + graph: nx.Graph, + graph_data: NetworkGraphData, + bus_id: str, + substation_id: str, +) -> tuple[pd.DataFrame, set[str], dict[str, object]]: + """Build the station-local busbar view and matching connection metadata. + + Parameters + ---------- + graph : nx.Graph + Graph representation of the node-breaker topology. + graph_data : NetworkGraphData + Graph data containing the station nodes used to derive busbars. + bus_id : str + Bus-branch bus identifier for the station view to build. + substation_id : str + Substation name used when deriving station-local busbars. + + Returns + ------- + tuple[pd.DataFrame, set[str], dict[str, object]] + The filtered busbar DataFrame with reassigned local integer ids, the selected + busbar ids, and the busbar connection info restricted to that station view. + """ + busbar_df = get_busbar_df(nodes_df=graph_data.nodes, substation_id=substation_id) + seed_busbar_ids = set(busbar_df.loc[busbar_df["bus_branch_bus_id"] == bus_id, "grid_model_id"]) + full_busbar_connection_info = get_busbar_connection_info(graph=graph) + selected_busbar_ids = _expand_busbars_connected_via_switches( + seed_busbar_ids=seed_busbar_ids, + full_busbar_connection_info=full_busbar_connection_info, + allowed_busbar_ids=set(busbar_df["grid_model_id"]), + ) + busbar_df = busbar_df[busbar_df["grid_model_id"].isin(selected_busbar_ids)].copy().reset_index(drop=True) + busbar_df["int_id"] = busbar_df.index + if busbar_df.empty: + raise ValueError(f"No busbars found for bus_id {bus_id} in substation {substation_id}") + + selected_busbar_ids = set(busbar_df["grid_model_id"]) + busbar_connection_info = { + busbar_grid_model_id: connection_info + for busbar_grid_model_id, connection_info in full_busbar_connection_info.items() + if busbar_grid_model_id in selected_busbar_ids + } + return busbar_df, selected_busbar_ids, busbar_connection_info + + +def _get_station_asset_bays( + switches_df: pd.DataFrame, + switchable_assets_df: pd.DataFrame, + busbar_df: pd.DataFrame, + edge_connection_info: dict[str, object], + substation_id: str, + selected_busbar_ids: set[str], +) -> tuple[dict[str, AssetBay], list[str]]: + """Build station-local asset bays filtered to the selected busbar view. + + Parameters + ---------- + switches_df : pd.DataFrame + Switch rows for the station topology. + switchable_assets_df : pd.DataFrame + Asset rows aligned with the station-local switching table. + busbar_df : pd.DataFrame + Filtered station-local busbar view. + edge_connection_info : dict[str, object] + Edge metadata used by ``get_asset_bay(...)`` to derive bay paths. + substation_id : str + Substation identifier owning the station-local asset bays. + selected_busbar_ids : set[str] + Busbar ids kept in the station-local busbar view. + + Returns + ------- + tuple[dict[str, AssetBay], list[str]] + Asset bays keyed by asset grid model id and the collected station log messages. + """ + station_logs: list[str] = [] + asset_bays_by_asset_id: dict[str, AssetBay] = {} + for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list(): + asset_bay, logs = get_asset_bay( + switches_df, + station_grid_model_id=substation_id, + asset_grid_model_id=asset_grid_model_id, + busbar_df=busbar_df, + edge_connection_info=edge_connection_info, + ) + station_logs.extend(logs) + if asset_bay is None: + continue + + station_local_sr_switches = { + busbar_grid_model_id: switch_grid_model_id + for busbar_grid_model_id, switch_grid_model_id in asset_bay.sr_switch_grid_model_id.items() + if busbar_grid_model_id in selected_busbar_ids + } + if len(station_local_sr_switches) == 0: + continue + + asset_bays_by_asset_id[asset_grid_model_id] = asset_bay.model_copy( + update={"sr_switch_grid_model_id": station_local_sr_switches}, + deep=True, + ) + + return asset_bays_by_asset_id, station_logs + + +def _get_station_asset_connections( + switchable_assets_df: pd.DataFrame, + asset_bays_by_asset_id: dict[str, AssetBay], +) -> tuple[list[MaterializedAssetConnection], list[MaterializedAssetConnection], list[bool]]: + """Materialize branch and injection connections aligned with station tables. + + Parameters + ---------- + switchable_assets_df : pd.DataFrame + Asset rows aligned with the station-local switching table. + asset_bays_by_asset_id : dict[str, AssetBay] + Station-local asset bays keyed by asset grid model id. + + Returns + ------- + tuple[list[MaterializedAssetConnection], list[MaterializedAssetConnection], list[bool]] + Branch connections, injection connections, and the branch mask aligned with + ``switchable_assets_df``. + """ + assets = [ + normalize_switchable_asset_payload(asset_payload) for asset_payload in switchable_assets_df.to_dict(orient="records") + ] + remove_suffix_from_switchable_assets(assets) + + branch_mask = [asset.is_branch() is not False for asset in assets] + branch_connections: list[MaterializedAssetConnection] = [] + injection_connections: list[MaterializedAssetConnection] = [] + for asset, is_branch in zip(assets, branch_mask, strict=True): + connection = MaterializedAssetConnection( + asset=asset, + asset_bay=asset_bays_by_asset_id.get(asset.grid_model_id), + ) + if is_branch: + branch_connections.append(connection) + else: + injection_connections.append(connection) + + return branch_connections, injection_connections, branch_mask + + def node_breaker_topology_to_graph_data(net: Network, substation_info: SubstationInformation) -> NetworkGraphData: """Convert a node breaker topology to a NetworkGraph. @@ -324,19 +472,12 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati graph_data = node_breaker_topology_to_graph_data(network, substation_info=station_info) graph = get_node_breaker_topology_graph(graph_data) - busbar_df = get_busbar_df(nodes_df=graph_data.nodes, substation_id=substation_id) - seed_busbar_ids = set(busbar_df.loc[busbar_df["bus_branch_bus_id"] == bus_id, "grid_model_id"]) - full_busbar_connection_info = get_busbar_connection_info(graph=graph) - selected_busbar_ids = _expand_busbars_connected_via_switches( - seed_busbar_ids=seed_busbar_ids, - full_busbar_connection_info=full_busbar_connection_info, - allowed_busbar_ids=set(busbar_df["grid_model_id"]), + busbar_df, selected_busbar_ids, busbar_connection_info = _get_station_busbar_view( + graph=graph, + graph_data=graph_data, + bus_id=bus_id, + substation_id=substation_id, ) - busbar_df = busbar_df[busbar_df["grid_model_id"].isin(selected_busbar_ids)].copy().reset_index(drop=True) - busbar_df["int_id"] = busbar_df.index - if busbar_df.empty: - raise ValueError(f"No busbars found for bus_id {bus_id} in substation {substation_id}") - selected_busbar_ids = set(busbar_df["grid_model_id"]) coupler_df = get_coupler_df( switches_df=graph_data.switches, busbar_df=busbar_df, substation_id=substation_id, graph=graph @@ -345,11 +486,6 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati if not coupler_df.empty: coupler_df[["busbar_from_id", "busbar_to_id"]] = coupler_df[["busbar_from_id", "busbar_to_id"]].astype(int) - busbar_connection_info = { - busbar_grid_model_id: connection_info - for busbar_grid_model_id, connection_info in full_busbar_connection_info.items() - if busbar_grid_model_id in selected_busbar_ids - } edge_connection_info = get_edge_connection_info(graph=graph) switchable_assets_df = get_switchable_asset(busbar_connection_info, graph_data.node_assets, graph_data.branches) connected_asset_ids = { @@ -361,27 +497,14 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati drop=True ) - asset_bay_dict = {} - for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list(): - asset_bay, logs = get_asset_bay( - graph_data.switches, - station_grid_model_id=substation_id, - asset_grid_model_id=asset_grid_model_id, - busbar_df=busbar_df, - edge_connection_info=edge_connection_info, - ) - station_logs.extend(logs) - if asset_bay is None: - continue - station_local_sr_switches = { - busbar_grid_model_id: switch_grid_model_id - for busbar_grid_model_id, switch_grid_model_id in asset_bay.sr_switch_grid_model_id.items() - if busbar_grid_model_id in selected_busbar_ids - } - if len(station_local_sr_switches) == 0: - continue - asset_bay = asset_bay.model_copy(update={"sr_switch_grid_model_id": station_local_sr_switches}, deep=True) - asset_bay_dict[asset_grid_model_id] = asset_bay + asset_bays_by_asset_id, station_logs = _get_station_asset_bays( + switches_df=graph_data.switches, + switchable_assets_df=switchable_assets_df, + busbar_df=busbar_df, + edge_connection_info=edge_connection_info, + substation_id=substation_id, + selected_busbar_ids=selected_busbar_ids, + ) asset_connectivity, asset_switching_table, busbar_connectivity, busbar_switching_table = get_station_connection_tables( busbar_connection_info, busbar_df=busbar_df, switchable_assets_df=switchable_assets_df @@ -390,13 +513,10 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati asset_switching_table = remove_double_connections(asset_switching_table, substation_id=substation_id) busbars = get_list_of_busbars_from_df(busbar_df) couplers = get_list_of_coupler_from_df(coupler_df) - assets = get_list_of_switchable_assets_from_df(station_branches=switchable_assets_df) - remove_suffix_from_switchable_assets(assets) - asset_bays_by_id = {asset_bay.asset_bay_id: asset_bay for asset_bay in asset_bay_dict.values()} - station_asset_bay_ids = [ - asset_bay_dict[asset_grid_model_id].asset_bay_id if asset_grid_model_id in asset_bay_dict else None - for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list() - ] + branch_connections, injection_connections, branch_mask = _get_station_asset_connections( + switchable_assets_df=switchable_assets_df, + asset_bays_by_asset_id=asset_bays_by_asset_id, + ) station = MaterializedStation( grid_model_id=bus_id, @@ -405,15 +525,12 @@ def get_station(network: Network, bus_id: str, station_info: SubstationInformati voltage_level=int(station_info.nominal_v), busbars=busbars, couplers=couplers, - asset_connections=[ - MaterializedAssetConnection( - asset=asset, - asset_bay=asset_bays_by_id.get(asset_bay_id) if asset_bay_id is not None else None, - ) - for asset, asset_bay_id in zip(assets, station_asset_bay_ids, strict=True) - ], - asset_switching_table=asset_switching_table, - asset_connectivity=asset_connectivity, + branch_connections=branch_connections, + injection_connections=injection_connections, + branch_switching_table=asset_switching_table[:, branch_mask], + injection_switching_table=asset_switching_table[:, [not is_branch for is_branch in branch_mask]], + branch_connectivity=asset_connectivity[:, branch_mask], + injection_connectivity=asset_connectivity[:, [not is_branch for is_branch in branch_mask]], busbar_switching_table=busbar_switching_table, busbar_connectivity=busbar_connectivity, model_log=station_logs, @@ -539,11 +656,15 @@ def get_topology(network: Network, network_masks: NetworkMasks, importer_paramet elif existing_asset_bay != asset_bay: raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + topology_assets_list = list(topology_assets.values()) + branch_assets = [asset for asset in topology_assets_list if asset.is_branch() is not False] + injection_assets = [asset for asset in topology_assets_list if asset.is_branch() is False] return Topology( topology_id=topology_id, grid_model_file=grid_model_file, raw_stations=topology_stations, - assets=list(topology_assets.values()), + branch_assets=branch_assets, + injection_assets=injection_assets, asset_bays=list(topology_asset_bays.values()), timestamp=timestamp, ) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index 65d7ce27d..484b22d3d 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -24,7 +24,6 @@ get_asset_switching_table, get_list_of_busbars_from_df, get_list_of_coupler_from_df, - get_list_of_switchable_assets_from_df, ) from toop_engine_importer.pandapower_import.pandapower_toolset_node_breaker import ( get_all_switches_from_bus_ids, @@ -37,6 +36,7 @@ MaterializedStation, Topology, build_asset_bay_id, + normalize_switchable_asset_payload, topology_parts_from_materialized_station, ) @@ -420,6 +420,78 @@ def get_parameter_from_station( return parameter +def _materialize_station_asset_connections( + station_branches: pd.DataFrame, + asset_connection_path: list[AssetBay | None], +) -> tuple[list[MaterializedAssetConnection], list[MaterializedAssetConnection], list[bool]]: + """Build split materialized asset connections aligned with the station branch rows. + + Parameters + ---------- + station_branches : pd.DataFrame + Station-local asset rows aligned with the switching matrix columns. + asset_connection_path : list[AssetBay | None] + Asset-bay metadata aligned with ``station_branches``. + + Returns + ------- + tuple[list[MaterializedAssetConnection], list[MaterializedAssetConnection], list[bool]] + Branch connections, injection connections, and the branch mask aligned with + ``station_branches``. + """ + switchable_assets = [ + normalize_switchable_asset_payload(asset_payload) for asset_payload in station_branches.to_dict(orient="records") + ] + asset_terminals = ( + station_branches["branch_end"].tolist() + if "branch_end" in station_branches.columns + else [None] * len(station_branches) + ) + branch_mask = [asset.is_branch() is not False for asset in switchable_assets] + branch_connections: list[MaterializedAssetConnection] = [] + injection_connections: list[MaterializedAssetConnection] = [] + for asset, asset_terminal, asset_bay, is_branch in zip( + switchable_assets, + asset_terminals, + asset_connection_path, + branch_mask, + strict=True, + ): + connection = MaterializedAssetConnection(asset=asset, terminal=asset_terminal, asset_bay=asset_bay) + if is_branch: + branch_connections.append(connection) + else: + injection_connections.append(connection) + return branch_connections, injection_connections, branch_mask + + +def _get_station_identity( + station_buses: pd.DataFrame, + voltage_level: float | int | str, +) -> dict[str, str | float | int]: + """Extract the station identity fields used to build the materialized station. + + Parameters + ---------- + station_buses : pd.DataFrame + Station bus rows used to derive the grid model id and display name. + voltage_level : float | int | str + Resolved station voltage level. + + Returns + ------- + dict[str, str | float | int] + Minimal station identity payload containing ``grid_model_id``, ``name``, and + ``voltage_level``. + """ + station_buses = station_buses.sort_index() + return { + "grid_model_id": station_buses["grid_model_id"].iloc[0], + "name": station_buses["name"].iloc[0], + "voltage_level": voltage_level, + } + + def get_station_from_id( network: pp.pandapowerNet, station_id_list: list[int], @@ -449,41 +521,24 @@ def get_station_from_id( asset_connection_path, ) = get_branches_from_station(network, station_buses, foreign_key=foreign_key) - # get the lists of the pydantic model objects - busbar_list = get_list_of_busbars_from_df(station_buses[station_buses["type"] == "b"]) - coupler_list = get_list_of_coupler_from_df(coupler_elements) - switchable_assets_list = get_list_of_switchable_assets_from_df( - station_branches=station_branches, asset_bay_list=asset_connection_path - ) - asset_terminals = ( - station_branches["branch_end"].tolist() - if "branch_end" in station_branches.columns - else [None] * len(station_branches) - ) - voltage_level_float = get_parameter_from_station(network=network, station_bus_index=station_id_list, parameter="vn_kv") - # region = get_parameter_from_station(network, station_name, "zone") - - # get the station_name from the station_id - # in pandapower a station is a bus -> only one entry in the DataFrame - station_buses.sort_index(inplace=True) - station_name = station_buses["name"].values[0] - grid_model_id = station_buses["grid_model_id"].values[0] + station_identity = _get_station_identity(station_buses=station_buses, voltage_level=voltage_level_float) + branch_connections, injection_connections, branch_mask = _materialize_station_asset_connections( + station_branches=station_branches, + asset_connection_path=asset_connection_path, + ) return MaterializedStation( - grid_model_id=grid_model_id, - name=station_name, + grid_model_id=station_identity["grid_model_id"], + name=station_identity["name"], # region=region, - voltage_level=voltage_level_float, - busbars=busbar_list, - couplers=coupler_list, - asset_connections=[ - MaterializedAssetConnection(asset=asset, terminal=asset_terminal, asset_bay=asset_bay) - for asset, asset_terminal, asset_bay in zip( - switchable_assets_list, asset_terminals, asset_connection_path, strict=True - ) - ], - asset_switching_table=switching_matrix, + voltage_level=station_identity["voltage_level"], + busbars=get_list_of_busbars_from_df(station_buses[station_buses["type"] == "b"]), + couplers=get_list_of_coupler_from_df(coupler_elements), + branch_connections=branch_connections, + injection_connections=injection_connections, + branch_switching_table=switching_matrix[:, branch_mask], + injection_switching_table=switching_matrix[:, [not is_branch for is_branch in branch_mask]], ) @@ -568,11 +623,15 @@ def get_asset_topology_from_network( elif existing_asset_bay != asset_bay: raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + topology_assets_list = list(topology_assets.values()) + branch_assets = [asset for asset in topology_assets_list if asset.is_branch() is not False] + injection_assets = [asset for asset in topology_assets_list if asset.is_branch() is False] return Topology( topology_id=topology_id, grid_model_file=grid_model_file, raw_stations=topology_stations, - assets=list(topology_assets.values()), + branch_assets=branch_assets, + injection_assets=injection_assets, asset_bays=list(topology_asset_bays.values()), timestamp=timestamp, ) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py index 47f2414ff..06f47864f 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py @@ -205,7 +205,13 @@ def preprocess_net_step2(network: pp.pandapowerNet, topology_model: Topology) -> raw_stations.append( station.model_copy(update={"grid_model_id": f"{new_id}{SEPARATOR}{station.grid_model_id.split(SEPARATOR)[1]}"}) ) - return copy_topology_with_updates(topology_model, raw_stations, topology_model.assets, topology_model.asset_bays) + return copy_topology_with_updates( + topology_model, + raw_stations, + topology_model.asset_bays, + branch_assets=topology_model.branch_assets, + injection_assets=topology_model.injection_assets, + ) def fuse_cross_coupler( @@ -273,19 +279,20 @@ def validate_asset_topology(net: pp.pandapowerNet, topology_model: Topology) -> """ for station in topology_model.materialize_stations(): s_id = int(station.grid_model_id.split(r"%%")[0]) + station_connections = [*station.branch_connections, *station.injection_connections] connection_dict = pp.toolbox.get_connected_elements_dict(net, [s_id]) del connection_dict["bus"] len_connection = len([element for key in connection_dict for element in connection_dict[key]]) - if len_connection != len(station.asset_connections): + if len_connection != len(station_connections): logger.warning( - f"Station {s_id} has {len(station.asset_connections)} assets but only " + f"Station {s_id} has {len(station_connections)} assets but only " + f"{len_connection} connections in the network", **connection_dict, ) - for asset_connection in station.asset_connections: + for asset_connection in station_connections: logger.warning(f"Station {s_id} with assets: {asset_connection.asset}", asset=asset_connection.asset) raise ValueError( - f"Station {s_id} has {len(station.asset_connections)} assets but only " + f"Station {s_id} has {len(station_connections)} assets but only " + f"{len_connection} connections in the network" ) diff --git a/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py b/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py index efc1d0bc7..b5ae7ae48 100644 --- a/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py +++ b/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py @@ -302,8 +302,8 @@ def test_get_changes_from_switching_table(ucte_asset_topology, caplog): result = get_changes_from_switching_table(station) assert result == expected - topology_stations[0].asset_switching_table[0][3] = False - topology_stations[0].asset_switching_table[1][3] = True + topology_stations[0].branch_switching_table[0][2] = False + topology_stations[0].branch_switching_table[1][2] = True station = topology_stations[0] expected = [ { @@ -317,15 +317,15 @@ def test_get_changes_from_switching_table(ucte_asset_topology, caplog): assert result == expected # Test case where asset is connected to multiple busbars (should raise ValueError) - topology_stations[0].asset_switching_table[0][3] = True - topology_stations[0].asset_switching_table[1][3] = True + topology_stations[0].branch_switching_table[0][2] = True + topology_stations[0].branch_switching_table[1][2] = True station = topology_stations[0] with pytest.raises(ValueError): get_changes_from_switching_table(station) # test case disconnected asset - topology_stations[0].asset_switching_table[0][3] = False - topology_stations[0].asset_switching_table[1][3] = False + topology_stations[0].branch_switching_table[0][2] = False + topology_stations[0].branch_switching_table[1][2] = False station = topology_stations[0] expected = [ { @@ -339,18 +339,20 @@ def test_get_changes_from_switching_table(ucte_asset_topology, caplog): assert result == expected # test where an asset is connected to two busbars within a station an is now reassigned - topology_stations[0].asset_switching_table[0][3] = False - topology_stations[0].asset_switching_table[1][3] = True - topology_stations[0].busbars.append(Busbar(grid_model_id="D8SU1_13", type=None, name="", int_id=0, in_service=True)) - topology_stations[0].assets[3].grid_model_id = "D8SU1_11 D8SU1_13 1" + topology_stations[0].branch_switching_table[0][2] = False + topology_stations[0].branch_switching_table[1][2] = True + topology_stations[0].busbars.append( + Busbar(grid_model_id="D8SU1_13", busbar_type=None, name="", int_id=0, in_service=True) + ) + topology_stations[0].branch_connections[2].asset.grid_model_id = "D8SU1_11 D8SU1_13 1" station = topology_stations[0] with pytest.raises(ValueError): get_changes_from_switching_table(station) # Test case where busbar connection is not found - topology_stations[0].asset_switching_table[0][3] = False - topology_stations[0].asset_switching_table[1][3] = True - topology_stations[0].assets[3].grid_model_id = "NOT_A_VALID_ID" + topology_stations[0].branch_switching_table[0][2] = False + topology_stations[0].branch_switching_table[1][2] = True + topology_stations[0].branch_connections[2].asset.grid_model_id = "NOT_A_VALID_ID" station = topology_stations[0] with pytest.raises(ValueError): get_changes_from_switching_table(station) @@ -552,11 +554,11 @@ def test_asset_topo_to_uct(ucte_asset_topology, ucte_file): # Test case where asset is reassigned # test trafo - topology_stations[0].asset_switching_table[0][3] = False - topology_stations[0].asset_switching_table[1][3] = True + topology_stations[0].branch_switching_table[0][2] = False + topology_stations[0].branch_switching_table[1][2] = True # test line - topology_stations[0].asset_switching_table[0][4] = True - topology_stations[0].asset_switching_table[1][4] = False + topology_stations[0].branch_switching_table[0][3] = True + topology_stations[0].branch_switching_table[1][3] = False # test coupler topology_stations[0].couplers[0].open = True @@ -565,7 +567,7 @@ def test_asset_topo_to_uct(ucte_asset_topology, ucte_file): preamble, nodes, lines, trafos, trafo_reg, postamble = parse_ucte(input_uct_contents) # test order change of line # original grid id: "D2SU1_31 D2SU1_31 2" - topology_stations[0].assets[1].grid_model_id = "D8SU1_12 D7SU1_11 1" + topology_stations[0].branch_connections[1].asset.grid_model_id = "D8SU1_12 D7SU1_11 1" lines.iloc[4, 2] = "1" lines.iloc[4, 0] = "D8SU1_11" lines.iloc[4, 1] = "D7SU2_11" diff --git a/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py b/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py index fbdfe6960..c146997c9 100644 --- a/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py +++ b/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py @@ -81,7 +81,7 @@ def test_get_busbar_df(network_graph_data_test1: NetworkGraphData): expected = [ { "grid_model_id": "BBS3_1", - "type": "busbar", + "busbar_type": "busbar", "name": "ab", "int_id": 0, "in_service": True, @@ -89,7 +89,7 @@ def test_get_busbar_df(network_graph_data_test1: NetworkGraphData): }, { "grid_model_id": "BBS3_2", - "type": "busbar", + "busbar_type": "busbar", "name": "cd", "int_id": 1, "in_service": True, @@ -108,7 +108,7 @@ def test_get_coupler_df_busbar_coupler(network_graph_for_asset_topo: tuple[nx.Gr expected = [ { "grid_model_id": "5", - "type": "BREAKER", + "coupler_type": "BREAKER", "name": "fid_5", "in_service": True, "open": False, @@ -131,13 +131,13 @@ def test_get_switchable_asset(network_graph_for_asset_topoV2_S1: tuple[nx.Graph, bus_info = get_busbar_connection_info(graph=graph) branches_df = network_graph_data.branches expected = [ - {"grid_model_id": "L1", "name": "", "type": "LINE", "in_service": True}, - {"grid_model_id": "L2", "name": "", "type": "LINE", "in_service": True}, - {"grid_model_id": "L3", "name": "", "type": "LINE", "in_service": True}, - {"grid_model_id": "L4", "name": "", "type": "LINE", "in_service": True}, - {"grid_model_id": "L5", "name": "", "type": "LINE", "in_service": True}, - {"grid_model_id": "generator1", "name": "", "type": "GENERATOR", "in_service": True}, - {"grid_model_id": "generator2", "name": "", "type": "GENERATOR", "in_service": True}, + {"grid_model_id": "L1", "name": "", "asset_type": "LINE", "in_service": True}, + {"grid_model_id": "L2", "name": "", "asset_type": "LINE", "in_service": True}, + {"grid_model_id": "L3", "name": "", "asset_type": "LINE", "in_service": True}, + {"grid_model_id": "L4", "name": "", "asset_type": "LINE", "in_service": True}, + {"grid_model_id": "L5", "name": "", "asset_type": "LINE", "in_service": True}, + {"grid_model_id": "generator1", "name": "", "asset_type": "GENERATOR", "in_service": True}, + {"grid_model_id": "generator2", "name": "", "asset_type": "GENERATOR", "in_service": True}, ] res = get_switchable_asset(busbar_connection_info=bus_info, node_assets_df=nodes_asset_df, branches_df=branches_df) assert res.to_dict(orient="records") == expected @@ -151,11 +151,17 @@ def test_switching_tables_V2(basic_node_breaker_network_powsybl_grid_v2): asset_connectivity = np.array([[True, True, True], [True, True, True], [True, True, True]]) asset_switching_table = np.array([[True, True, False], [False, False, False], [False, False, True]]) - assert np.array_equal(station.asset_connectivity, asset_connectivity) - assert np.array_equal(station.asset_switching_table, asset_switching_table) + assert len(station.branch_connections) == 2 + assert len(station.injection_connections) == 1 + assert np.array_equal( + np.concatenate([station.branch_connectivity, station.injection_connectivity], axis=1), asset_connectivity + ) + assert np.array_equal( + np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1), asset_switching_table + ) assert len(station.couplers) == 2 - assert station.couplers[0].type == "BREAKER" - assert station.couplers[1].type == "BREAKER" + assert station.couplers[0].coupler_type == "BREAKER" + assert station.couplers[1].coupler_type == "BREAKER" station_info = {"name": "Station6", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL6"} station_info = SubstationInformation(**station_info) @@ -169,8 +175,14 @@ def test_switching_tables_V2(basic_node_breaker_network_powsybl_grid_v2): [[True, True], [False, False], [False, False], [True, True], [False, False], [False, False]] ) - assert np.array_equal(station.asset_connectivity, asset_connectivity) - assert np.array_equal(station.asset_switching_table, asset_switching_table) + assert len(station.branch_connections) == 1 + assert len(station.injection_connections) == 1 + assert np.array_equal( + np.concatenate([station.branch_connectivity, station.injection_connectivity], axis=1), asset_connectivity + ) + assert np.array_equal( + np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1), asset_switching_table + ) def test_station_coupler(basic_node_breaker_network_powsybl_grid_v2): @@ -183,13 +195,13 @@ def test_station_coupler(basic_node_breaker_network_powsybl_grid_v2): assert len(station.couplers) == 2 assert station.couplers[0].grid_model_id == "VL2_BREAKER" - assert station.couplers[0].type == "BREAKER" + assert station.couplers[0].coupler_type == "BREAKER" assert station.couplers[0].name == "VL2_BREAKER" assert station.couplers[0].busbar_from_id == 0 assert station.couplers[0].busbar_to_id == 1 assert station.couplers[1].grid_model_id == "VL2_BREAKER#0" - assert station.couplers[1].type == "BREAKER" + assert station.couplers[1].coupler_type == "BREAKER" assert station.couplers[1].name == "VL2_BREAKER#0" assert station.couplers[1].busbar_from_id == 1 assert station.couplers[1].busbar_to_id == 2 @@ -202,14 +214,14 @@ def test_station_coupler(basic_node_breaker_network_powsybl_grid_v2): assert len(station.couplers) == 5 assert station.couplers[0].grid_model_id == "VL6_BREAKER" - assert station.couplers[0].type == "BREAKER" + assert station.couplers[0].coupler_type == "BREAKER" assert station.couplers[0].name == "VL6_BREAKER" assert station.couplers[0].busbar_from_id == 1 assert station.couplers[0].busbar_to_id == 4 assert not station.couplers[0].open assert station.couplers[1].grid_model_id == "VL6_BREAKER_1_1" - assert station.couplers[1].type == "BREAKER" + assert station.couplers[1].coupler_type == "BREAKER" assert station.couplers[1].name == "VL6_BREAKER_1_1" assert station.couplers[1].busbar_from_id == 0 assert station.couplers[1].busbar_to_id == 1 @@ -217,7 +229,7 @@ def test_station_coupler(basic_node_breaker_network_powsybl_grid_v2): assert station.couplers[1].in_service assert station.couplers[2].grid_model_id == "VL6_BREAKER_2_1" - assert station.couplers[2].type == "BREAKER" + assert station.couplers[2].coupler_type == "BREAKER" assert station.couplers[2].name == "VL6_BREAKER_2_1" assert station.couplers[2].busbar_from_id == 3 assert station.couplers[2].busbar_to_id == 4 @@ -225,13 +237,13 @@ def test_station_coupler(basic_node_breaker_network_powsybl_grid_v2): assert station.couplers[2].in_service assert station.couplers[3].grid_model_id == "VL6_DISCONNECTOR_2_4" - assert station.couplers[3].type == "DISCONNECTOR" + assert station.couplers[3].coupler_type == "DISCONNECTOR" assert station.couplers[3].name == "VL6_DISCONNECTOR_2_4" assert station.couplers[3].busbar_from_id == 1 assert station.couplers[3].busbar_to_id == 2 assert station.couplers[4].grid_model_id == "VL6_DISCONNECTOR_3_5" - assert station.couplers[4].type == "DISCONNECTOR" + assert station.couplers[4].coupler_type == "DISCONNECTOR" assert station.couplers[4].name == "VL6_DISCONNECTOR_3_5" assert station.couplers[4].busbar_from_id == 4 assert station.couplers[4].busbar_to_id == 5 @@ -242,19 +254,19 @@ def test_station_coupler(basic_node_breaker_network_powsybl_grid_v2): station = get_station(net, "VL4_0", station_info) assert len(station.couplers) == 3 assert station.couplers[0].grid_model_id == "VL4_BREAKER" - assert station.couplers[0].type == "BREAKER" + assert station.couplers[0].coupler_type == "BREAKER" assert station.couplers[0].name == "VL4_BREAKER" assert station.couplers[0].busbar_from_id == 0 assert station.couplers[0].busbar_to_id == 1 assert station.couplers[1].grid_model_id == "VL4_BREAKER#0" - assert station.couplers[1].type == "BREAKER" + assert station.couplers[1].coupler_type == "BREAKER" assert station.couplers[1].name == "VL4_BREAKER#0" assert station.couplers[1].busbar_from_id == 0 assert station.couplers[1].busbar_to_id == 2 assert station.couplers[2].grid_model_id == "VL4_BREAKER#1" - assert station.couplers[2].type == "BREAKER" + assert station.couplers[2].coupler_type == "BREAKER" assert station.couplers[2].name == "VL4_BREAKER#1" assert station.couplers[2].busbar_from_id == 0 assert station.couplers[2].busbar_to_id == 3 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 c07f17599..ccd84dabe 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 @@ -9,6 +9,7 @@ from pathlib import Path import networkx as nx +import numpy as np import pandas as pd import pypowsybl import pytest @@ -45,6 +46,18 @@ ) +def all_station_connections(station: MaterializedStation): + return [*station.branch_connections, *station.injection_connections] + + +def all_station_switching_table(station: MaterializedStation): + return np.concatenate([station.branch_switching_table, station.injection_switching_table], axis=1) + + +def all_station_connectivity(station: MaterializedStation): + return np.concatenate([station.branch_connectivity, station.injection_connectivity], axis=1) + + def test_node_breaker_topology_to_graph(basic_node_breaker_network_powsybl_grid): net = basic_node_breaker_network_powsybl_grid substation_dict = {"name": "Station1", "region": "BE", "nominal_v": 380, "voltage_level_id": "VL1"} @@ -139,12 +152,12 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): busbars = res.busbars assert len(busbars) == 2 assert busbars[0].grid_model_id == "BBS3_1" - assert busbars[0].type == "busbar" + assert busbars[0].busbar_type == "busbar" assert busbars[0].name == "bus1" assert busbars[0].int_id == 0 assert busbars[0].in_service is True assert busbars[1].grid_model_id == "BBS3_2" - assert busbars[1].type == "busbar" + assert busbars[1].busbar_type == "busbar" assert busbars[1].name == "bus2" assert busbars[1].int_id == 1 assert busbars[1].in_service is True @@ -152,20 +165,20 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): couplers = res.couplers assert len(couplers) == 1 assert couplers[0].grid_model_id == "VL3_BREAKER" - assert couplers[0].type == "BREAKER" + assert couplers[0].coupler_type == "BREAKER" assert couplers[0].name == "VL3_BREAKER" assert couplers[0].busbar_from_id == 0 assert couplers[0].busbar_to_id == 1 assert not couplers[0].open assert couplers[0].in_service - assets = [asset_connection.asset for asset_connection in res.asset_connections] - asset_bays = [asset_connection.asset_bay for asset_connection in res.asset_connections] - asset_terminals = [asset_connection.terminal for asset_connection in res.asset_connections] + assets = [asset_connection.asset for asset_connection in all_station_connections(res)] + asset_bays = [asset_connection.asset_bay for asset_connection in all_station_connections(res)] + asset_terminals = [asset_connection.terminal for asset_connection in all_station_connections(res)] assert len(assets) == 5 assert asset_terminals == [None] * len(assets) assert assets[0].grid_model_id == "L3" - assert assets[0].type == "LINE" + assert assets[0].asset_type == "LINE" assert assets[0].name == "" assert assets[0].in_service assert asset_bays[0].sl_switch_grid_model_id is None @@ -176,7 +189,7 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): } assert assets[1].grid_model_id == "L6" - assert assets[1].type == "LINE" + assert assets[1].asset_type == "LINE" assert assets[1].name == "" assert assets[1].in_service assert asset_bays[1].sl_switch_grid_model_id is None @@ -187,7 +200,7 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): } assert assets[2].grid_model_id == "L7" - assert assets[2].type == "LINE" + assert assets[2].asset_type == "LINE" assert assets[2].name == "" assert assets[2].in_service assert asset_bays[2].sl_switch_grid_model_id is None @@ -198,7 +211,7 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): } assert assets[3].grid_model_id == "L9" - assert assets[3].type == "LINE" + assert assets[3].asset_type == "LINE" assert assets[3].name == "" assert assets[3].in_service assert asset_bays[3].sl_switch_grid_model_id is None @@ -209,7 +222,7 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): } assert assets[4].grid_model_id == "load2" - assert assets[4].type == "LOAD" + assert assets[4].asset_type == "LOAD" assert assets[4].name == "" assert assets[4].in_service assert asset_bays[4].sl_switch_grid_model_id is None @@ -219,13 +232,15 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): "BBS3_2": "load2_DISCONNECTOR_13_1", } - assert len(res.asset_switching_table) == 2 - assert list(res.asset_switching_table[0]) == [True, True, False, True, False] - assert list(res.asset_switching_table[1]) == [False, False, True, False, True] + switching_table = all_station_switching_table(res) + assert len(switching_table) == 2 + assert list(switching_table[0]) == [True, True, False, True, False] + assert list(switching_table[1]) == [False, False, True, False, True] - assert len(res.asset_connectivity) == 2 - assert list(res.asset_connectivity[0]) == [True, True, True, True, True] - assert list(res.asset_connectivity[1]) == [True, True, True, True, True] + connectivity = all_station_connectivity(res) + assert len(connectivity) == 2 + assert list(connectivity[0]) == [True, True, True, True, True] + assert list(connectivity[1]) == [True, True, True, True, True] @pytest.mark.skip(reason="Known limitation in the current implementation") @@ -236,12 +251,12 @@ def test_get_station_edge_cases_one_bay_two_assets(asset_topo_edge_cases_node_br res = get_station(net, "VL1_1", station_info) load_assets = [ asset_connection.asset - for asset_connection in res.asset_connections + for asset_connection in all_station_connections(res) if "load" in asset_connection.asset.grid_model_id ] load_asset_bays = [ asset_connection.asset_bay - for asset_connection in res.asset_connections + for asset_connection in all_station_connections(res) if "load" in asset_connection.asset.grid_model_id ] assert len(load_assets) == 2, "Expected two loads" @@ -272,11 +287,11 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): assert all( coupler.busbar_from_id in busbar_int_ids and coupler.busbar_to_id in busbar_int_ids for coupler in res.couplers ) - assert len([coupler for coupler in res.couplers if coupler.type == "BREAKER"]) > 0 - assert res.asset_switching_table.shape == (len(res.busbars), len(res.asset_connections)) - assert res.asset_connectivity.shape == (len(res.busbars), len(res.asset_connections)) + assert len([coupler for coupler in res.couplers if coupler.coupler_type == "BREAKER"]) > 0 + assert all_station_switching_table(res).shape == (len(res.busbars), len(all_station_connections(res))) + assert all_station_connectivity(res).shape == (len(res.busbars), len(all_station_connections(res))) - for asset_bay in [asset_connection.asset_bay for asset_connection in res.asset_connections]: + for asset_bay in [asset_connection.asset_bay for asset_connection in all_station_connections(res)]: if asset_bay is None: continue assert set(asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids @@ -310,11 +325,11 @@ def test_get_station_edge_cases(asset_topo_edge_cases_node_breaker_grid): coupler.busbar_from_id in busbar_int_ids and coupler.busbar_to_id in busbar_int_ids for coupler in res.couplers ) - assert res.asset_switching_table.shape == (len(res.busbars), len(res.asset_connections)) - assert res.asset_connectivity.shape == (len(res.busbars), len(res.asset_connections)) - assert res.asset_connectivity.any(axis=0).all() + assert all_station_switching_table(res).shape == (len(res.busbars), len(all_station_connections(res))) + assert all_station_connectivity(res).shape == (len(res.busbars), len(all_station_connections(res))) + assert all_station_connectivity(res).any(axis=0).all() - for asset_bay in [asset_connection.asset_bay for asset_connection in res.asset_connections]: + for asset_bay in [asset_connection.asset_bay for asset_connection in all_station_connections(res)]: if asset_bay is None: continue assert set(asset_bay.sr_switch_grid_model_id) <= busbar_grid_model_ids diff --git a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py index 437182061..055a09a15 100644 --- a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py +++ b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py @@ -298,7 +298,7 @@ def test_get_station_from_id(pp_network_w_switches): assert isinstance(result, MaterializedStation) assert result.grid_model_id == r"0%%bus" assert result.name == "Double Busbar 1" - assert result.type is None + assert result.station_type is None assert result.voltage_level == 380.0 assert len(result.busbars) == 2 diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index 7734af476..dd500d8f1 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -72,22 +72,27 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow Busbar(int_id=1, grid_model_id="bb1", bus_branch_bus_id=buses.index[1]), ], couplers=[], - asset_connections=[StationAssetConnection(asset_id="asset0", terminal=None, asset_bay_id=None)], - asset_switching_table=np.array([[True], [False]]), + branch_connections=[StationAssetConnection(asset_id="asset0", terminal=None, asset_bay_id=None)], + injection_connections=[], + branch_switching_table=np.array([[True], [False]]), + injection_switching_table=np.zeros((2, 0), dtype=bool), ) regular_station = RawStation( grid_model_id=buses.index[1], name="regular", busbars=[Busbar(int_id=0, grid_model_id="bb2", bus_branch_bus_id=buses.index[1])], couplers=[], - asset_connections=[StationAssetConnection(asset_id="asset1", terminal=None, asset_bay_id=None)], - asset_switching_table=np.array([[True]]), + branch_connections=[StationAssetConnection(asset_id="asset1", terminal=None, asset_bay_id=None)], + injection_connections=[], + branch_switching_table=np.array([[True]]), + injection_switching_table=np.zeros((1, 0), dtype=bool), ) topology_model = Topology( topology_id="topology", grid_model_file="grid.xiidm", raw_stations=[split_station, regular_station], - assets=[SwitchableAsset(grid_model_id="asset0"), SwitchableAsset(grid_model_id="asset1")], + branch_assets=[SwitchableAsset(grid_model_id="asset0"), SwitchableAsset(grid_model_id="asset1")], + injection_assets=[], timestamp=datetime.now(), ) diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py index efa42745f..d68d06580 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py @@ -7,17 +7,28 @@ """Contains the data models for the asset topology.""" +from copy import deepcopy from datetime import datetime from enum import Enum import numpy as np -from beartype.typing import Any, Literal, Optional, TypeAlias, Union, get_args +from beartype.typing import Any, Collection, Literal, Optional, TypeAlias, Union, get_args from numpydantic import NDArray, Shape from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator StationSwitchingArray: TypeAlias = NDArray[Shape["* n_bus, * n_asset"], np.bool_] +def _merged_round_trip_payload(model: BaseModel, update: Optional[dict[str, Any]], *, deep: bool = False) -> dict[str, Any]: + """Merge model field values and requested updates for revalidation-aware model_copy overrides.""" + payload = {field_name: getattr(model, field_name) for field_name in type(model).model_fields} + if deep: + payload = deepcopy(payload) + if update: + payload.update(update) + return payload + + class PowsyblSwitchValues(Enum): """Enum for the switch values in the Powsybl model.""" @@ -35,6 +46,7 @@ class PowsyblSwitchValues(Enum): "trafo3w_mv", "trafo3w_hv", "impedance", + "xward", ] AssetBranchTypePowsybl: TypeAlias = Literal[ "LINE", @@ -52,7 +64,6 @@ class PowsyblSwitchValues(Enum): "ward", "ward_load", "ward_shunt", - "xward", "xward_load", "xward_shunt", "dcline_from", @@ -78,7 +89,7 @@ class Busbar(BaseModel): """ The unique identifier of the busbar. Corresponds to the busbar's id in the grid model.""" - type: Optional[str] = None + busbar_type: Optional[str] = None """ The type of the busbar, might be useful for finding the busbar later on """ name: Optional[str] = None @@ -194,7 +205,7 @@ class BusbarCoupler(BaseModel): """ The unique identifier of the coupler. Corresponds to the coupler's id in the grid model.""" - type: Optional[str] = None + coupler_type: Optional[str] = None """ The type of the coupler, might be useful for finding the coupler later on """ name: Optional[str] = None @@ -243,7 +254,7 @@ class SwitchableAsset(BaseModel): """ The unique identifier of the asset. Corresponds to the asset's id in the grid model.""" - type: Optional[AssetType] = None + asset_type: Optional[AssetType] = None """ The type of the asset. These refer loosely to the types in the pandapower/powsybl grid models. If set, this can be used to disambiguate branches from injections """ @@ -265,9 +276,45 @@ def is_branch(self) -> Optional[bool]: bool True if the asset is a branch, False if it is an injection. """ - if self.type is None: + if self.asset_type is None: return None - return self.type in get_args(AssetBranchType) + return self.asset_type in get_args(AssetBranchType) + + +class BranchAsset(SwitchableAsset): + """Switchable asset representing a branch-type element.""" + + asset_type: Optional[AssetBranchType] = None + + def is_branch(self) -> Optional[bool]: + """Return branch semantics for branch assets.""" + return None if self.asset_type is None else True + + +class InjectionAsset(SwitchableAsset): + """Switchable asset representing an injection-type element.""" + + asset_type: Optional[AssetInjectionType] = None + + def is_branch(self) -> Optional[bool]: + """Return branch semantics for injection assets.""" + return None if self.asset_type is None else False + + +def normalize_switchable_asset_payload(asset: dict[str, Any]) -> SwitchableAsset: + """Normalize an asset payload to the matching branch or injection subclass when possible.""" + if isinstance(asset, (BranchAsset, InjectionAsset)): + return asset + + asset_data = asset.model_dump() if isinstance(asset, SwitchableAsset) else dict(asset) + if "asset_type" not in asset_data and "type" in asset_data: + asset_data["asset_type"] = asset_data.pop("type") + asset_type = asset_data.get("asset_type") + if asset_type in get_args(AssetBranchType): + return BranchAsset(**asset_data) + if asset_type in get_args(AssetInjectionType): + return InjectionAsset(**asset_data) + return SwitchableAsset(**asset_data) class StationAssetConnection(BaseModel): @@ -295,6 +342,12 @@ class MaterializedAssetConnection(BaseModel): asset_bay: Optional[AssetBay] = None """Optional station-local asset bay payload for this station-local asset occurrence.""" + def get_sr_switch(self) -> Optional[dict[str, str]]: + """Return the sr_switch_grid_model_id dict from the asset bay if it exists.""" + if self.asset_bay is not None: + return self.asset_bay.sr_switch_grid_model_id + return None + class AssetSetpoint(BaseModel): """Asset data describing a single asset with a setpoint. @@ -308,7 +361,7 @@ class AssetSetpoint(BaseModel): """ The unique identifier of the asset. Corresponds to the asset's id in the grid model.""" - type: Optional[str] = None + asset_type: Optional[str] = None """ The type of the asset, might be useful for finding the asset later on """ name: Optional[str] = None @@ -325,6 +378,7 @@ def _validate_station_switching_tables( asset_count: int, asset_switching_table: np.ndarray, asset_connectivity: Optional[np.ndarray], + asset_kind: str, ) -> None: """Validate switching-table shapes against the station dimensions. @@ -342,6 +396,8 @@ def _validate_station_switching_tables( Current station switching table. asset_connectivity : Optional[np.ndarray] Optional connectivity mask describing physically allowed assignments. + asset_kind: str + The kind of asset being validated, used in error messages (e.g. "branch" or "injection"). Returns ------- @@ -355,15 +411,15 @@ def _validate_station_switching_tables( """ if asset_switching_table.shape != (busbar_count, asset_count): raise ValueError( - f"asset_switching_table shape {asset_switching_table.shape} does not match busbars " - f"{busbar_count} and assets {asset_count}" + f"{asset_kind}_switching_table shape {asset_switching_table.shape} does not match busbars " + f"{busbar_count} and {asset_kind} assets {asset_count}" f" Station_id: {station_grid_model_id}, Name: {station_name}" ) if asset_connectivity is not None and asset_connectivity.shape != (busbar_count, asset_count): raise ValueError( - f"asset_connectivity shape {asset_connectivity.shape} does not match busbars " - f"{busbar_count} and assets {asset_count}" + f"{asset_kind}_connectivity shape {asset_connectivity.shape} does not match busbars " + f"{busbar_count} and {asset_kind} assets {asset_count}" f" Station_id: {station_grid_model_id}, Name: {station_name}" ) @@ -373,6 +429,7 @@ def _validate_station_physical_assignments( station_name: Optional[str], asset_switching_table: np.ndarray, asset_connectivity: Optional[np.ndarray], + asset_kind: str, ) -> None: """Validate that all current assignments are physically allowed. @@ -386,6 +443,8 @@ def _validate_station_physical_assignments( Current station switching table. asset_connectivity : Optional[np.ndarray] Optional connectivity mask describing physically allowed assignments. + asset_kind: str + The kind of asset being validated, used in error messages (e.g. "branch" or "injection"). Returns ------- @@ -400,7 +459,7 @@ def _validate_station_physical_assignments( if asset_connectivity is not None: if np.logical_and(asset_switching_table, np.logical_not(asset_connectivity)).any(): raise ValueError( - f"Not all current assignments are physically allowed " + f"Not all current {asset_kind} assignments are physically allowed " f"Station_id: {station_grid_model_id}, Name: {station_name}" ) @@ -424,7 +483,7 @@ class _StationStructure(BaseModel): name: Optional[str] = None """The name of the station.""" - type: Optional[str] = None + station_type: Optional[str] = None """The type of the station.""" region: Optional[str] = None @@ -439,8 +498,8 @@ class _StationStructure(BaseModel): couplers: list[BusbarCoupler] """The list of couplers at the station.""" - asset_switching_table: StationSwitchingArray - """Holds the switching of each asset to each busbar, shape (n_bus, n_asset). + branch_switching_table: StationSwitchingArray + """Holds the switching of each branch asset to each busbar, shape (n_bus, n_branch_asset). An entry is true if the asset is connected to the busbar. Note: An asset can be connected to multiple busbars, in which case a closed coupler is assumed @@ -453,12 +512,14 @@ class _StationStructure(BaseModel): in_service for intentional disconnections. """ - asset_connectivity: Optional[StationSwitchingArray] = None - """Holds the all possible layouts of the asset_switching_table, shape (n_bus, n_asset). + injection_switching_table: StationSwitchingArray + """Holds the switching of each injection asset to each busbar, shape (n_bus, n_injection_asset).""" - An entry is true if it is possible to connect an asset to the busbar. - If None, it is assumed that all branches can be connected to all busbars. - """ + branch_connectivity: Optional[StationSwitchingArray] = None + """Holds all physically possible branch layouts, shape (n_bus, n_branch_asset).""" + + injection_connectivity: Optional[StationSwitchingArray] = None + """Holds all physically possible injection layouts, shape (n_bus, n_injection_asset).""" model_log: Optional[list[str]] = None """Holds log messages from the model creation process. @@ -467,6 +528,20 @@ class _StationStructure(BaseModel): A potential use case is to inform the user about data quality issues e.g. missing the Asset Bay switches. """ + @field_validator( + "branch_switching_table", + "injection_switching_table", + "branch_connectivity", + "injection_connectivity", + mode="before", + ) + @classmethod + def normalize_station_tables(cls, v: Optional[Collection]) -> Optional[np.ndarray]: + """Normalize split station tables to boolean numpy arrays.""" + if v is None: + return None + return np.asarray(v, dtype=bool) + @field_validator("busbars") @classmethod def check_int_id_unique(cls, v: list[Busbar]) -> list[Busbar]: @@ -544,8 +619,11 @@ class MaterializedStation(_StationStructure): station view; they are not intended to define a topology-owned canonical asset list. """ - asset_connections: list[MaterializedAssetConnection] - """Station-local asset payloads aligned with the switching tables.""" + branch_connections: list[MaterializedAssetConnection] = Field(default_factory=list) + """Station-local branch payloads aligned with ``branch_switching_table``.""" + + injection_connections: list[MaterializedAssetConnection] = Field(default_factory=list) + """Station-local injection payloads aligned with ``injection_switching_table``.""" @model_validator(mode="after") def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": @@ -554,15 +632,33 @@ def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": station_grid_model_id=self.grid_model_id, station_name=self.name, busbar_count=len(self.busbars), - asset_count=len(self.asset_connections), - asset_switching_table=self.asset_switching_table, - asset_connectivity=self.asset_connectivity, + asset_count=len(self.branch_connections), + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", ) _validate_station_physical_assignments( station_grid_model_id=self.grid_model_id, station_name=self.name, - asset_switching_table=self.asset_switching_table, - asset_connectivity=self.asset_connectivity, + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", + ) + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.injection_connections), + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", ) return self @@ -570,7 +666,7 @@ def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": """Check if the asset bay bus is in busbars.""" busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] - for asset_connection in self.asset_connections: + for asset_connection in [*self.branch_connections, *self.injection_connections]: asset = asset_connection.asset asset_bay = asset_connection.asset_bay if asset_bay is not None: @@ -603,15 +699,75 @@ def __eq__(self, other: object) -> bool: and self.region == other.region and self.busbars == other.busbars and self.couplers == other.couplers - and self.asset_connections == other.asset_connections - and np.array_equal(self.asset_switching_table, other.asset_switching_table) + and self.branch_connections == other.branch_connections + and self.injection_connections == other.injection_connections + and np.array_equal(self.branch_switching_table, other.branch_switching_table) + and np.array_equal(self.injection_switching_table, other.injection_switching_table) + and ( + np.array_equal(self.branch_connectivity, other.branch_connectivity) + if (self.branch_connectivity is not None and other.branch_connectivity is not None) + else self.branch_connectivity == other.branch_connectivity + ) and ( - np.array_equal(self.asset_connectivity, other.asset_connectivity) - if (self.asset_connectivity is not None and other.asset_connectivity is not None) - else self.asset_connectivity == other.asset_connectivity + np.array_equal(self.injection_connectivity, other.injection_connectivity) + if (self.injection_connectivity is not None and other.injection_connectivity is not None) + else self.injection_connectivity == other.injection_connectivity ) ) + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "MaterializedStation": + """Copy and revalidate the station.""" + payload = _merged_round_trip_payload(self, update, deep=deep) + return type(self).model_validate(payload) + + def get_connected_assets( + self, + busbar_index: int, + topology_assets: Optional[list[SwitchableAsset]] = None, + asset_scope: Literal["all", "branch", "injection"] = "all", + ) -> list[SwitchableAsset]: + """Return in-service assets connected to one busbar. + + Parameters + ---------- + busbar_index : int + Row index into the station switching tables. + topology_assets : Optional[list[SwitchableAsset]] + Ignored for materialized stations because payloads are embedded locally. + asset_scope : Literal["all", "branch", "injection"] + Restrict the lookup to branch or injection connections. + + Returns + ------- + list[SwitchableAsset] + Connected in-service assets for the requested busbar and scope. + """ + del topology_assets + if asset_scope == "branch": + return [ + asset_connection.asset + for asset_connection, is_connected in zip( + self.branch_connections, + self.branch_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_connection.asset.in_service + ] + if asset_scope == "injection": + return [ + asset_connection.asset + for asset_connection, is_connected in zip( + self.injection_connections, + self.injection_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_connection.asset.in_service + ] + return [ + *self.get_connected_assets(busbar_index, asset_scope="branch"), + *self.get_connected_assets(busbar_index, asset_scope="injection"), + ] + class RawStation(_StationStructure): """Station data stored inside a topology without embedded asset payloads. @@ -621,23 +777,26 @@ class RawStation(_StationStructure): SwitchableAsset payloads. """ - asset_connections: list[StationAssetConnection] - """Station-local asset references aligned with the switching tables.""" + branch_connections: list[StationAssetConnection] = Field(default_factory=list) + """Station-local branch references aligned with ``branch_switching_table``.""" + + injection_connections: list[StationAssetConnection] = Field(default_factory=list) + """Station-local injection references aligned with ``injection_switching_table``.""" def with_asset_terminals(self, asset_terminals: list[Optional[BranchEnd]]) -> "RawStation": - """Return a copy with updated station-local branch terminals.""" - if len(asset_terminals) != len(self.asset_connections): + """Return a copy with updated branch terminals.""" + if len(asset_terminals) != len(self.branch_connections): raise ValueError( - f"asset_terminals length {len(asset_terminals)} does not match asset_connections length " - f"{len(self.asset_connections)}" + f"asset_terminals length {len(asset_terminals)} does not match branch_connections length " + f"{len(self.branch_connections)}" f" Station_id: {self.grid_model_id}, Name: {self.name}" ) return self.model_copy( update={ - "asset_connections": [ + "branch_connections": [ asset_connection.model_copy(update={"terminal": asset_terminal}) - for asset_connection, asset_terminal in zip(self.asset_connections, asset_terminals, strict=True) + for asset_connection, asset_terminal in zip(self.branch_connections, asset_terminals, strict=True) ] } ) @@ -649,15 +808,33 @@ def check_asset_reference_alignment(self: "RawStation") -> "RawStation": station_grid_model_id=self.grid_model_id, station_name=self.name, busbar_count=len(self.busbars), - asset_count=len(self.asset_connections), - asset_switching_table=self.asset_switching_table, - asset_connectivity=self.asset_connectivity, + asset_count=len(self.branch_connections), + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", ) _validate_station_physical_assignments( station_grid_model_id=self.grid_model_id, station_name=self.name, - asset_switching_table=self.asset_switching_table, - asset_connectivity=self.asset_connectivity, + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", + ) + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.injection_connections), + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", ) return self @@ -668,21 +845,89 @@ def __eq__(self, other: object) -> bool: return ( self.grid_model_id == other.grid_model_id and self.name == other.name - and self.type == other.type + and self.station_type == other.station_type and self.region == other.region and self.voltage_level == other.voltage_level and self.busbars == other.busbars and self.couplers == other.couplers - and self.asset_connections == other.asset_connections - and np.array_equal(self.asset_switching_table, other.asset_switching_table) + and self.branch_connections == other.branch_connections + and self.injection_connections == other.injection_connections + and np.array_equal(self.branch_switching_table, other.branch_switching_table) + and np.array_equal(self.injection_switching_table, other.injection_switching_table) + and ( + np.array_equal(self.branch_connectivity, other.branch_connectivity) + if (self.branch_connectivity is not None and other.branch_connectivity is not None) + else self.branch_connectivity == other.branch_connectivity + ) and ( - np.array_equal(self.asset_connectivity, other.asset_connectivity) - if (self.asset_connectivity is not None and other.asset_connectivity is not None) - else self.asset_connectivity == other.asset_connectivity + np.array_equal(self.injection_connectivity, other.injection_connectivity) + if (self.injection_connectivity is not None and other.injection_connectivity is not None) + else self.injection_connectivity == other.injection_connectivity ) and self.model_log == other.model_log ) + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "RawStation": + """Copy and revalidate the station.""" + payload = _merged_round_trip_payload(self, update, deep=deep) + return type(self).model_validate(payload) + + def get_connected_assets( + self, + busbar_index: int, + topology_assets: Optional[list[SwitchableAsset]] = None, + asset_scope: Literal["all", "branch", "injection"] = "all", + ) -> list[SwitchableAsset]: + """Return in-service topology assets connected to one busbar. + + Parameters + ---------- + busbar_index : int + Row index into the station switching tables. + topology_assets : Optional[list[SwitchableAsset]] + Topology-owned assets used to resolve raw station asset references. + asset_scope : Literal["all", "branch", "injection"] + Restrict the lookup to branch or injection connections. + + Returns + ------- + list[SwitchableAsset] + Connected in-service assets for the requested busbar and scope. + + Raises + ------ + ValueError + If topology assets are missing for a raw station lookup. + """ + if topology_assets is None: + raise ValueError("topology_assets must be provided when resolving connected assets for a RawStation") + + asset_map = {asset.grid_model_id: asset for asset in topology_assets} + if asset_scope == "branch": + return [ + asset_map[asset_connection.asset_id] + for asset_connection, is_connected in zip( + self.branch_connections, + self.branch_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_map[asset_connection.asset_id].in_service + ] + if asset_scope == "injection": + return [ + asset_map[asset_connection.asset_id] + for asset_connection, is_connected in zip( + self.injection_connections, + self.injection_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_map[asset_connection.asset_id].in_service + ] + return [ + *self.get_connected_assets(busbar_index, topology_assets=topology_assets, asset_scope="branch"), + *self.get_connected_assets(busbar_index, topology_assets=topology_assets, asset_scope="injection"), + ] + class Topology(BaseModel): """Topology data describing a single timestep topology. @@ -709,8 +954,11 @@ class Topology(BaseModel): Each raw station represents one bus-branch bus view of a splitable station. """ - assets: list[SwitchableAsset] = Field(default_factory=list) - """The topology-owned canonical asset payloads. + branch_assets: list[SwitchableAsset] = Field(default_factory=list) + """The topology-owned canonical branch payloads.""" + + injection_assets: list[SwitchableAsset] = Field(default_factory=list) + """The topology-owned canonical injection payloads. Station-local branch-end and asset-bay assignment data are stored on raw_stations instead of on these canonical payloads. @@ -730,13 +978,26 @@ class Topology(BaseModel): metrics: Optional[dict[str, float]] = None """ The metrics of the topology. """ - @field_validator("assets") + @field_validator("branch_assets") + @classmethod + def check_branch_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + """Check if all topology branch assets have unique grid model ids.""" + asset_ids = [asset.grid_model_id for asset in v] + if len(asset_ids) != len(set(asset_ids)): + raise ValueError("grid_model_id must be unique for topology branch assets") + if any(asset.is_branch() is False for asset in v): + raise ValueError("branch_assets must not contain injection assets") + return v + + @field_validator("injection_assets") @classmethod - def check_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: - """Check if all topology assets have unique grid model ids.""" + def check_injection_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + """Check if all topology injection assets have unique grid model ids.""" asset_ids = [asset.grid_model_id for asset in v] if len(asset_ids) != len(set(asset_ids)): - raise ValueError("grid_model_id must be unique for topology assets") + raise ValueError("grid_model_id must be unique for topology injection assets") + if any(asset.is_branch() is True for asset in v): + raise ValueError("injection_assets must not contain branch assets") return v @field_validator("asset_bays") @@ -753,18 +1014,26 @@ def check_asset_bay_ids_unique(cls, v: list[AssetBay]) -> list[AssetBay]: @model_validator(mode="after") def check_station_asset_references(self: "Topology") -> "Topology": """Check if all station asset references exist in the topology-owned collections.""" - asset_ids = {asset.grid_model_id for asset in self.assets} + branch_asset_ids = {asset.grid_model_id for asset in self.branch_assets} + injection_asset_ids = {asset.grid_model_id for asset in self.injection_assets} asset_bay_ids = {asset_bay.asset_bay_id for asset_bay in self.asset_bays} for station in self.raw_stations: - for asset_connection in station.asset_connections: + for asset_connection in station.branch_connections: asset_id = asset_connection.asset_id - if asset_id not in asset_ids: + if asset_id not in branch_asset_ids: raise ValueError( - f"Asset grid_model_id {asset_id} referenced by station " + f"Branch asset grid_model_id {asset_id} referenced by station " f"{station.grid_model_id} does not exist in topology assets" ) - for asset_connection in station.asset_connections: + for asset_connection in station.injection_connections: + asset_id = asset_connection.asset_id + if asset_id not in injection_asset_ids: + raise ValueError( + f"Injection asset grid_model_id {asset_id} referenced by station " + f"{station.grid_model_id} does not exist in topology assets" + ) + for asset_connection in [*station.branch_connections, *station.injection_connections]: asset_bay_id = asset_connection.asset_bay_id if asset_bay_id is not None and asset_bay_id not in asset_bay_ids: raise ValueError( @@ -776,46 +1045,73 @@ def check_station_asset_references(self: "Topology") -> "Topology": def materialize_stations(self) -> list[MaterializedStation]: """Materialize station-local asset payloads from topology-owned assets and asset bays.""" - asset_map = {asset.grid_model_id: asset for asset in self.assets} + branch_asset_map = {asset.grid_model_id: asset for asset in self.branch_assets} + injection_asset_map = {asset.grid_model_id: asset for asset in self.injection_assets} asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} materialized_stations: list[MaterializedStation] = [] for station in self.raw_stations: - station_assets = [ - asset_map[asset_connection.asset_id].model_copy(deep=True) for asset_connection in station.asset_connections + station_branch_assets = [ + branch_asset_map[asset_connection.asset_id].model_copy(deep=True) + for asset_connection in station.branch_connections + ] + station_injection_assets = [ + injection_asset_map[asset_connection.asset_id].model_copy(deep=True) + for asset_connection in station.injection_connections ] - station_asset_bays = [ + station_branch_asset_bays = [ + asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) + if asset_connection.asset_bay_id is not None + else None + for asset_connection in station.branch_connections + ] + station_injection_asset_bays = [ asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) if asset_connection.asset_bay_id is not None else None - for asset_connection in station.asset_connections + for asset_connection in station.injection_connections ] materialized_stations.append( MaterializedStation( grid_model_id=station.grid_model_id, name=station.name, - type=station.type, + station_type=station.station_type, region=station.region, voltage_level=station.voltage_level, busbars=station.busbars, couplers=station.couplers, - asset_connections=[ + branch_connections=[ MaterializedAssetConnection( asset=asset, terminal=asset_connection.terminal, asset_bay=asset_bay, ) for asset, asset_connection, asset_bay in zip( - station_assets, - station.asset_connections, - station_asset_bays, + station_branch_assets, + station.branch_connections, + station_branch_asset_bays, strict=True, ) ], - asset_switching_table=station.asset_switching_table, - asset_connectivity=station.asset_connectivity, + injection_connections=[ + MaterializedAssetConnection( + asset=asset, + terminal=asset_connection.terminal, + asset_bay=asset_bay, + ) + for asset, asset_connection, asset_bay in zip( + station_injection_assets, + station.injection_connections, + station_injection_asset_bays, + strict=True, + ) + ], + branch_switching_table=station.branch_switching_table, + injection_switching_table=station.injection_switching_table, + branch_connectivity=station.branch_connectivity, + injection_connectivity=station.injection_connectivity, model_log=station.model_log, ) ) @@ -838,7 +1134,7 @@ def get_asset_bay_ids_for_asset(self, asset_grid_model_id: str) -> list[str]: asset_bay_ids: list[str] = [] seen_ids: set[str] = set() for station in self.raw_stations: - for asset_connection in station.asset_connections: + for asset_connection in [*station.branch_connections, *station.injection_connections]: asset_id = asset_connection.asset_id asset_bay_id = asset_connection.asset_bay_id if asset_id != asset_grid_model_id or asset_bay_id is None or asset_bay_id in seen_ids: @@ -863,6 +1159,11 @@ def get_asset_bays_for_asset(self, asset_grid_model_id: str) -> list[AssetBay]: asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} return [asset_bay_map[asset_bay_id] for asset_bay_id in self.get_asset_bay_ids_for_asset(asset_grid_model_id)] + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "Topology": + """Copy and revalidate the topology.""" + payload = _merged_round_trip_payload(self, update, deep=deep) + return type(self).model_validate(payload) + def build_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, occurrence_index: int = 0) -> str: """Create a deterministic station-scoped asset bay identifier. @@ -902,11 +1203,13 @@ def topology_parts_from_materialized_station( tuple[RawStation, list[SwitchableAsset], list[AssetBay]] Raw station record plus topology-owned assets and asset bays derived from the station. """ - assets: list[SwitchableAsset] = [] + branch_assets: list[SwitchableAsset] = [] + injection_assets: list[SwitchableAsset] = [] asset_bays: list[AssetBay] = [] - asset_connections: list[StationAssetConnection] = [] - for asset_connection in station.asset_connections: - asset = asset_connection.asset + branch_connections: list[StationAssetConnection] = [] + injection_connections: list[StationAssetConnection] = [] + for asset_connection in station.branch_connections: + asset = normalize_switchable_asset_payload(asset_connection.asset.model_dump(round_trip=True)) asset_bay = asset_connection.asset_bay asset_id = asset.grid_model_id asset_bay_id: Optional[str] = None @@ -915,8 +1218,26 @@ def topology_parts_from_materialized_station( asset_bay_id = asset_bay.asset_bay_id asset_bays.append(asset_bay.model_copy(deep=True)) - assets.append(asset.model_copy(deep=True)) - asset_connections.append( + branch_assets.append(asset.model_copy(deep=True)) + branch_connections.append( + StationAssetConnection( + asset_id=asset_id, + terminal=asset_connection.terminal, + asset_bay_id=asset_bay_id, + ) + ) + + for asset_connection in station.injection_connections: + asset = normalize_switchable_asset_payload(asset_connection.asset.model_dump(round_trip=True)) + asset_bay = asset_connection.asset_bay + asset_id = asset.grid_model_id + asset_bay_id = asset_bay.asset_bay_id if asset_bay is not None else None + + if asset_bay is not None: + asset_bays.append(asset_bay.model_copy(deep=True)) + + injection_assets.append(asset.model_copy(deep=True)) + injection_connections.append( StationAssetConnection( asset_id=asset_id, terminal=asset_connection.terminal, @@ -928,17 +1249,20 @@ def topology_parts_from_materialized_station( RawStation( grid_model_id=station.grid_model_id, name=station.name, - type=station.type, + station_type=station.station_type, region=station.region, voltage_level=station.voltage_level, busbars=station.busbars, couplers=station.couplers, - asset_connections=asset_connections, - asset_switching_table=station.asset_switching_table, - asset_connectivity=station.asset_connectivity, + branch_connections=branch_connections, + injection_connections=injection_connections, + branch_switching_table=station.branch_switching_table, + injection_switching_table=station.injection_switching_table, + branch_connectivity=station.branch_connectivity, + injection_connectivity=station.injection_connectivity, model_log=station.model_log, ), - assets, + [*branch_assets, *injection_assets], asset_bays, ) @@ -959,13 +1283,15 @@ def topology_from_materialized_stations(reference_topology: Topology, stations: Topology containing raw stations and topology-owned payloads derived from ``stations``. """ topology_stations: list[RawStation] = [] - topology_assets: dict[str, SwitchableAsset] = {} + topology_branch_assets: dict[str, SwitchableAsset] = {} + topology_injection_assets: dict[str, SwitchableAsset] = {} topology_asset_bays: dict[str, AssetBay] = {} for station in stations: topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) topology_stations.append(topology_station) for asset in station_assets: + topology_assets = topology_branch_assets if asset.is_branch() is not False else topology_injection_assets existing_asset = topology_assets.get(asset.grid_model_id) if existing_asset is None: topology_assets[asset.grid_model_id] = asset @@ -983,7 +1309,8 @@ def topology_from_materialized_stations(reference_topology: Topology, stations: grid_model_file=reference_topology.grid_model_file, name=reference_topology.name, raw_stations=topology_stations, - assets=list(topology_assets.values()), + branch_assets=list(topology_branch_assets.values()), + injection_assets=list(topology_injection_assets.values()), asset_bays=list(topology_asset_bays.values()), asset_setpoints=reference_topology.asset_setpoints, timestamp=reference_topology.timestamp, @@ -994,8 +1321,10 @@ def topology_from_materialized_stations(reference_topology: Topology, stations: def copy_topology_with_updates( reference_topology: Topology, raw_stations: list[RawStation], - assets: list[SwitchableAsset], asset_bays: list[AssetBay], + *, + branch_assets: Optional[list[SwitchableAsset]] = None, + injection_assets: Optional[list[SwitchableAsset]] = None, ) -> Topology: """Create a validated topology copy with updated payloads. @@ -1005,22 +1334,28 @@ def copy_topology_with_updates( Reference topology providing shared metadata. raw_stations : list[RawStation] Raw stations to set on the copied topology. - assets : list[SwitchableAsset] - Topology-owned assets to set on the copied topology. asset_bays : list[AssetBay] Topology-owned asset bays to set on the copied topology. + branch_assets : Optional[list[SwitchableAsset]], optional + Topology-owned branch assets to set on the copied topology. + injection_assets : Optional[list[SwitchableAsset]], optional + Topology-owned injection assets to set on the copied topology. Returns ------- Topology Validated topology copy with updated topology-owned payloads. """ + resolved_branch_assets = branch_assets if branch_assets is not None else reference_topology.branch_assets + resolved_injection_assets = injection_assets if injection_assets is not None else reference_topology.injection_assets + return Topology( topology_id=reference_topology.topology_id, grid_model_file=reference_topology.grid_model_file, name=reference_topology.name, raw_stations=raw_stations, - assets=assets, + branch_assets=resolved_branch_assets, + injection_assets=resolved_injection_assets, asset_bays=asset_bays, asset_setpoints=reference_topology.asset_setpoints, timestamp=reference_topology.timestamp, @@ -1057,7 +1392,7 @@ class Strategy(BaseModel): class AppliedStation(BaseModel): - """A realized station, including the new station and the changes made to the original station""" + """A realized station, including the new station and the changes made to the original station.""" station: MaterializedStation """The realized asset station object""" @@ -1065,15 +1400,17 @@ class AppliedStation(BaseModel): coupler_diff: list[BusbarCoupler] """A list of couplers that have been switched.""" - reassignment_diff: list[tuple[int, int, bool]] - """A list of reassignments that have been made. Each tuple contains the asset index that was - affected (not the asset grid_model_id but the index into the asset_switching_table), the busbar - index (again the index into the switching table) and whether the asset was connected (True) or - disconnected (False) to that busbar.""" + branch_reassignment_diff: list[tuple[int, int, bool]] + """Branch reassignments as ``(branch_index, busbar_index, connected)`` tuples.""" + + injection_reassignment_diff: list[tuple[int, int, bool]] + """Injection reassignments as ``(injection_index, busbar_index, connected)`` tuples.""" - disconnection_diff: list[int] - """A list of disconnections that have been made. Each tuple contains the asset index that was - disconnected.""" + branch_disconnection_diff: list[int] + """Branch indices that were disconnected.""" + + injection_disconnection_diff: list[int] + """Injection indices that were disconnected.""" class RealizedTopology(BaseModel): @@ -1091,12 +1428,14 @@ class RealizedTopology(BaseModel): """A list of couplers that have been switched. Each tuple contains the station grid_model_id and the coupler that was switched.""" - reassignment_diff: list[tuple[str, int, int, bool]] - """A list of reassignments that have been made. Each tuple contains the station grid_model_id, - the asset index that was affected (not the asset grid_model_id but the index into the - asset_switching_table), the busbar index (again the index into the switching table) and whether the - asset was connected (True) or disconnected (False) to that busbar.""" + branch_reassignment_diff: list[tuple[str, int, int, bool]] + """Branch reassignments as ``(station_id, branch_index, busbar_index, connected)`` tuples.""" + + injection_reassignment_diff: list[tuple[str, int, int, bool]] + """Injection reassignments as ``(station_id, injection_index, busbar_index, connected)`` tuples.""" + + branch_disconnection_diff: list[tuple[str, int]] + """Branch disconnections as ``(station_id, branch_index)`` tuples.""" - disconnection_diff: list[tuple[str, int]] - """A list of disconnections that have been made. Each tuple contains the station grid_model_id - and the asset index that was disconnected. This can also include non-relevant stations.""" + injection_disconnection_diff: list[tuple[str, int]] + """Injection disconnections as ``(station_id, injection_index)`` tuples.""" diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py index 03e06af49..5de71c319 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py @@ -5,7 +5,7 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 -"""Collects some common helper functions for asset topology manipulation.""" +"""Common helper functions for asset topology manipulation.""" import itertools from numbers import Integral @@ -21,8 +21,8 @@ AssetBay, Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, - RawStation, RealizedTopology, SwitchableAsset, Topology, @@ -39,18 +39,18 @@ def electrical_components(station: MaterializedStation, min_num_assets: int = 1) Parameters ---------- - station : Station - The station object to analyze. + station : MaterializedStation + Station to analyze. min_num_assets : int, optional - The minimum number of assets connected to a component to be considered a valid component, by default 1 + Minimum number of connected assets required for a component to be kept. Returns ------- list[list[int]] - A list of lists, where each inner list contains the indices of the busbars in the component indexing into the list - of all busbars in the station. + Busbar index groups, where each inner list references positions in + `station.busbars`. """ - n_connections_per_bus = station.asset_switching_table.sum(axis=1) + n_connections_per_bus = station.branch_switching_table.sum(axis=1) + station.injection_switching_table.sum(axis=1) int_id_mapper = {busbar.int_id: i for i, busbar in enumerate(station.busbars)} @@ -76,15 +76,15 @@ def electrical_components(station: MaterializedStation, min_num_assets: int = 1) def number_of_splits(station: MaterializedStation) -> int: - """Compute the number of electrical components that are present in a station. + """Compute the number of electrical components in a station. A set of busbars is considered a separate electrical component if it is not connected through a closed coupler to other busbars and there are at least two assets connected to the component. Parameters ---------- - station : Station - The station object to analyze. + station : MaterializedStation + Station to analyze. Returns ------- @@ -109,15 +109,15 @@ def remove_busbar(station: MaterializedStation, grid_model_id: str) -> Materiali Parameters ---------- - station : Station - The station object to modify. + station : MaterializedStation + Station to modify. grid_model_id : str - The grid_model_id of the busbar to remove. + Grid model identifier of the busbar to remove. Returns ------- - Station - The modified station object with the busbar removed. + MaterializedStation + Copy of `station` with the busbar removed. """ # Store the index and int_id of the dropped busbar index = [b.grid_model_id for b in station.busbars].index(grid_model_id) @@ -125,13 +125,28 @@ def remove_busbar(station: MaterializedStation, grid_model_id: str) -> Materiali busbars = [b for b in station.busbars if b.grid_model_id != grid_model_id] couplers = [c for c in station.couplers if int_id not in (c.busbar_from_id, c.busbar_to_id)] - asset_switching_table = np.delete(station.asset_switching_table, index, axis=0) - asset_connectivity = ( - np.delete(station.asset_connectivity, index, axis=0) if station.asset_connectivity is not None else None + branch_switching_table = np.delete(station.branch_switching_table, index, axis=0) + injection_switching_table = np.delete(station.injection_switching_table, index, axis=0) + branch_connectivity = ( + np.delete(station.branch_connectivity, index, axis=0) if station.branch_connectivity is not None else None + ) + injection_connectivity = ( + np.delete(station.injection_connectivity, index, axis=0) if station.injection_connectivity is not None else None ) def filter_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: - """Filter out the sr keys from the asset bay.""" + """Drop SR switch references to the removed busbar. + + Parameters + ---------- + asset_bay : Optional[AssetBay] + Asset bay whose SR switch references should be filtered. + + Returns + ------- + Optional[AssetBay] + Updated asset bay without references to the removed busbar. + """ if asset_bay is None: return None return asset_bay.model_copy( @@ -144,9 +159,13 @@ def filter_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: } ) - asset_connections = [ + branch_connections = [ + asset_connection.model_copy(update={"asset_bay": filter_sr_keys(asset_connection.asset_bay)}) + for asset_connection in station.branch_connections + ] + injection_connections = [ asset_connection.model_copy(update={"asset_bay": filter_sr_keys(asset_connection.asset_bay)}) - for asset_connection in station.asset_connections + for asset_connection in station.injection_connections ] # Create a new station object with the modified busbars, couplers, and asset switching table @@ -154,9 +173,12 @@ def filter_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: update={ "busbars": busbars, "couplers": couplers, - "asset_connections": asset_connections, - "asset_switching_table": asset_switching_table, - "asset_connectivity": asset_connectivity, + "branch_connections": branch_connections, + "injection_connections": injection_connections, + "branch_switching_table": branch_switching_table, + "injection_switching_table": injection_switching_table, + "branch_connectivity": branch_connectivity, + "injection_connectivity": injection_connectivity, } ) return new_station @@ -167,29 +189,44 @@ def filter_out_of_service_assets(station: MaterializedStation) -> MaterializedSt Parameters ---------- - station : Station - The station object to filter. + station : MaterializedStation + Station to filter. Returns ------- - Station - The new station object with all out-of-service assets removed. + MaterializedStation + Copy of `station` without out-of-service asset connections. """ - if all(asset_connection.asset.in_service for asset_connection in station.asset_connections): + if all(asset_connection.asset.in_service for asset_connection in station.branch_connections) and all( + asset_connection.asset.in_service for asset_connection in station.injection_connections + ): return station - in_service_asset_connections = [ - asset_connection for asset_connection in station.asset_connections if asset_connection.asset.in_service + branch_mask = [asset_connection.asset.in_service for asset_connection in station.branch_connections] + injection_mask = [asset_connection.asset.in_service for asset_connection in station.injection_connections] + branch_connections = [ + asset_connection + for asset_connection, in_service in zip(station.branch_connections, branch_mask, strict=True) + if in_service + ] + injection_connections = [ + asset_connection + for asset_connection, in_service in zip(station.injection_connections, injection_mask, strict=True) + if in_service ] - in_service_assets = [asset_connection.asset.in_service for asset_connection in station.asset_connections] return station.model_copy( update={ - "asset_connections": in_service_asset_connections, - "asset_switching_table": station.asset_switching_table[:, in_service_assets], - "asset_connectivity": station.asset_connectivity[:, in_service_assets] - if station.asset_connectivity is not None - else None, + "branch_connections": branch_connections, + "injection_connections": injection_connections, + "branch_switching_table": station.branch_switching_table[:, branch_mask], + "injection_switching_table": station.injection_switching_table[:, injection_mask], + "branch_connectivity": ( + station.branch_connectivity[:, branch_mask] if station.branch_connectivity is not None else None + ), + "injection_connectivity": ( + station.injection_connectivity[:, injection_mask] if station.injection_connectivity is not None else None + ), } ) @@ -199,13 +236,13 @@ def filter_out_of_service_busbars(station: MaterializedStation) -> MaterializedS Parameters ---------- - station : Station - The station object to filter. + station : MaterializedStation + Station to filter. Returns ------- - Station - The new station object with all out-of-service busbars removed. + MaterializedStation + Copy of `station` without out-of-service busbars. """ deleted_busbar_ids = [busbar.grid_model_id for busbar in station.busbars if not busbar.in_service] @@ -220,13 +257,13 @@ def filter_out_of_service_couplers(station: MaterializedStation) -> Materialized Parameters ---------- - station : Station - The station object to filter. + station : MaterializedStation + Station to filter. Returns ------- - Station - The new station object with all out-of-service couplers removed. + MaterializedStation + Copy of `station` without out-of-service couplers. """ if all(coupler.in_service for coupler in station.couplers): return station @@ -248,13 +285,13 @@ def filter_out_of_service(station: MaterializedStation) -> MaterializedStation: Parameters ---------- - station : Station - The station object to filter. + station : MaterializedStation + Station to filter. Returns ------- - Station - The new station object with all out-of-service assets removed. + MaterializedStation + Copy of `station` without out-of-service assets, busbars, or couplers. """ station = filter_out_of_service_couplers(station) station = filter_out_of_service_assets(station) @@ -269,25 +306,24 @@ def filter_out_of_service(station: MaterializedStation) -> MaterializedStation: def filter_duplicate_couplers( station: MaterializedStation, retain_type_hierarchy: Optional[list[str]] = None ) -> tuple[MaterializedStation, list[BusbarCoupler]]: - """Filter out duplicate couplers + """Filter out duplicate couplers. - Two couplers are considered duplicates if they connect the same busbars, regardless of their - order. If a duplicate coupler is found, only the first one is kept. + Two couplers are duplicates if they connect the same busbar pair, regardless of order. Parameters ---------- - station : Station - The station object to filter. + station : MaterializedStation + Station to filter. retain_type_hierarchy : Optional[list[str]], optional - If provided, not the first coupler is kept but the one with the highest type in the hierarchy list. Highest means - that the type is the first in the list. If not provided, the first coupler is kept, by default None + Optional priority order for retained coupler types. Earlier entries have + higher priority. Returns ------- - Station - The new station object with duplicate couplers removed. + MaterializedStation + Station with duplicate couplers removed. list[BusbarCoupler] - The list of removed couplers. + Removed duplicate couplers. """ # A dict with the coupler representation as key and a list of indices of the couplers coupler_dict: dict[tuple[int, int], list[int]] = {} @@ -310,7 +346,9 @@ def filter_duplicate_couplers( sorted_couplers = sorted( (station.couplers[i] for i in index), key=lambda c: ( - retain_type_hierarchy.index(c.type) if c.type in retain_type_hierarchy else len(retain_type_hierarchy) + retain_type_hierarchy.index(c.coupler_type) + if c.coupler_type in retain_type_hierarchy + else len(retain_type_hierarchy) ), ) # Keep the first coupler and remove the others @@ -346,24 +384,23 @@ def filter_disconnected_busbars( Parameters ---------- - station : Station - The station object potentially with disconnected busbars. + station : MaterializedStation + Station that may contain disconnected busbars. respect_coupler_open : bool, optional - If True, only closed couplers are considered connected, if False, all couplers are - considered connected, by default False + If `True`, only closed couplers contribute to connectivity. Returns ------- - Station - The new station object with disconnected busbars removed. + MaterializedStation + Station with disconnected busbars removed. list[Busbar] - The list of removed busbars. + Removed busbars. """ couplers = [ coupler for coupler in station.couplers if (not respect_coupler_open or not coupler.open) and coupler.in_service ] graph = nx.Graph() - num_assets_per_busbar = station.asset_switching_table.sum(axis=1) + num_assets_per_busbar = station.branch_switching_table.sum(axis=1) + station.injection_switching_table.sum(axis=1) graph.add_nodes_from( [ (busbar.int_id, {"num_assets": num_assets}) @@ -388,19 +425,19 @@ def filter_disconnected_busbars( def reindex_busbars(station: MaterializedStation) -> MaterializedStation: - """Reindex the int-ids of the busbars in the station + """Reindex the int-ids of the busbars in the station. This might be necessary after filder_disconnected_busbars or filter_out_of_service have been called. Parameters ---------- - station : Station - The station object with possible inconsistent busbar ids. + station : MaterializedStation + Station with potentially non-contiguous busbar ids. Returns ------- - Station - The new station object with reindexed busbars, where int-ids are continuous and start at 0. + MaterializedStation + Copy of `station` with contiguous busbar `int_id` values starting at 0. """ busbar_mapping = {busbar.int_id: i for i, busbar in enumerate(station.busbars)} new_busbars = [busbar.model_copy(update={"int_id": i}) for i, busbar in enumerate(station.busbars)] @@ -422,49 +459,69 @@ def reindex_busbars(station: MaterializedStation) -> MaterializedStation: def filter_assets_by_type( station: MaterializedStation, assets_allowed: set[str], allow_none_type: bool = False ) -> tuple[MaterializedStation, list[SwitchableAsset]]: - """Filter assets by type + """Filter assets by type. Removes all assets that have a type which is not in the set of allowed types. Parameters ---------- - station : Station - The station object to filter. + station : MaterializedStation + Station to filter. assets_allowed : set[str] - The set of asset types that are allowed. + Allowed asset types. allow_none_type : bool, optional - If True, assets without a type are allowed, by default False + If `True`, assets without an `asset_type` are retained. Returns ------- - Station - The new station object with assets of the wrong type removed. + MaterializedStation + Station with disallowed assets removed. list[SwitchableAsset] - The list of removed assets. + Removed assets. """ - asset_mask = [ - (asset_connection.asset.type in assets_allowed) or (allow_none_type and asset_connection.asset.type is None) - for asset_connection in station.asset_connections + branch_mask = [ + (asset_connection.asset.asset_type in assets_allowed) + or (allow_none_type and asset_connection.asset.asset_type is None) + for asset_connection in station.branch_connections + ] + injection_mask = [ + (asset_connection.asset.asset_type in assets_allowed) + or (allow_none_type and asset_connection.asset.asset_type is None) + for asset_connection in station.injection_connections ] - if all(asset_mask): + if all(branch_mask) and all(injection_mask): return station, [] removed_assets = [ asset_connection.asset - for asset_connection, mask in zip(station.asset_connections, asset_mask, strict=True) + for asset_connection, mask in zip(station.branch_connections, branch_mask, strict=True) + if not mask + ] + [ + asset_connection.asset + for asset_connection, mask in zip(station.injection_connections, injection_mask, strict=True) if not mask ] - kept_asset_connections = [ - asset_connection for asset_connection, mask in zip(station.asset_connections, asset_mask, strict=True) if mask + branch_connections = [ + asset_connection for asset_connection, mask in zip(station.branch_connections, branch_mask, strict=True) if mask + ] + injection_connections = [ + asset_connection + for asset_connection, mask in zip(station.injection_connections, injection_mask, strict=True) + if mask ] new_station = station.model_copy( update={ - "asset_connections": kept_asset_connections, - "asset_switching_table": station.asset_switching_table[:, asset_mask], - "asset_connectivity": station.asset_connectivity[:, asset_mask] - if station.asset_connectivity is not None - else None, + "branch_connections": branch_connections, + "injection_connections": injection_connections, + "branch_switching_table": station.branch_switching_table[:, branch_mask], + "injection_switching_table": station.injection_switching_table[:, injection_mask], + "branch_connectivity": ( + station.branch_connectivity[:, branch_mask] if station.branch_connectivity is not None else None + ), + "injection_connectivity": ( + station.injection_connectivity[:, injection_mask] if station.injection_connectivity is not None else None + ), } ) return new_station, removed_assets @@ -473,52 +530,60 @@ def filter_assets_by_type( def find_multi_connected_without_coupler( station: MaterializedStation, ) -> list[tuple[Integral, Integral, Integral]]: - """Find assets bridging multiple busbars without a coupler in between + """Find assets that bridge multiple busbars without an intervening coupler. - These cases can cause a bug in the downstream functions + These cases can cause problems in downstream processing. Parameters ---------- - station : Station - The station object to check. + station : MaterializedStation + Station to inspect. Returns ------- list[tuple[Integral, Integral, Integral]] - A list of tuples containing the index of the multi-connected asset and the indices of the - two busbars it is bridging. - Only returns bridges that don't have a coupler in between. - The first busbar index will always be lower than the second, hence a routine which always - removes the first doesn't run into double removals if an asset appears multiple times. + Tuples of `(asset_index, lower_busbar_index, upper_busbar_index)` for + multi-connected assets that bridge busbars without a coupler. """ multi_connected_without_coupler = [] - for asset_idx in np.flatnonzero(station.asset_switching_table.sum(axis=0) > 1): - busbars_bridged = np.flatnonzero(station.asset_switching_table[:, asset_idx]).tolist() - for bus1_idx, bus2_idx in itertools.combinations(busbars_bridged, 2): - # bus1_idx shall always be the smaller of the two - if bus1_idx > bus2_idx: - smaller_bus_idx, larger_bus_idx = bus2_idx, bus1_idx - else: - smaller_bus_idx, larger_bus_idx = bus1_idx, bus2_idx - if not any( - ( - coupler.busbar_from_id == station.busbars[smaller_bus_idx].int_id - and coupler.busbar_to_id == station.busbars[larger_bus_idx].int_id - ) - or ( - coupler.busbar_from_id == station.busbars[larger_bus_idx].int_id - and coupler.busbar_to_id == station.busbars[smaller_bus_idx].int_id - ) - for coupler in station.couplers - ): - multi_connected_without_coupler.append( + def _append_multi_connected(asset_table: np.ndarray, asset_index_offset: int) -> None: + """Collect multi-busbar asset bridges that have no coupler between them. + + Parameters + ---------- + asset_table : np.ndarray + Switching table to inspect. + asset_index_offset : int + Offset used to map local asset indices to station-wide asset indices. + """ + for local_asset_idx in np.flatnonzero(asset_table.sum(axis=0) > 1): + busbars_bridged = np.flatnonzero(asset_table[:, local_asset_idx]).tolist() + for bus1_idx, bus2_idx in itertools.combinations(busbars_bridged, 2): + smaller_bus_idx = min(bus1_idx, bus2_idx) + larger_bus_idx = max(bus1_idx, bus2_idx) + + if not any( ( - asset_idx, - smaller_bus_idx, - larger_bus_idx, + coupler.busbar_from_id == station.busbars[smaller_bus_idx].int_id + and coupler.busbar_to_id == station.busbars[larger_bus_idx].int_id ) - ) + or ( + coupler.busbar_from_id == station.busbars[larger_bus_idx].int_id + and coupler.busbar_to_id == station.busbars[smaller_bus_idx].int_id + ) + for coupler in station.couplers + ): + multi_connected_without_coupler.append( + ( + int(local_asset_idx + asset_index_offset), + smaller_bus_idx, + larger_bus_idx, + ) + ) + + _append_multi_connected(station.branch_switching_table, 0) + _append_multi_connected(station.injection_switching_table, len(station.branch_connections)) return multi_connected_without_coupler @@ -526,80 +591,104 @@ def find_multi_connected_without_coupler( def fix_multi_connected_without_coupler( station: MaterializedStation, ) -> tuple[MaterializedStation, list[tuple[SwitchableAsset, Busbar, Busbar]]]: - """Remove one connection for multi-connected assets without a coupler in between + """Remove one connection for unsupported multi-connected assets. Will always remove the connection to the busbar with the lower index. Parameters ---------- - station : Station - The station object to fix + station : MaterializedStation + Station to fix. Returns ------- - Station - The new station object with the multi-connected assets fixed. + MaterializedStation + Station with unsupported multi-connected assets reduced to one busbar. list[tuple[SwitchableAsset, Busbar, Busbar]] - A list of tuples containing the previously multi-connected assets and the busbars they were - connected to. The first busbar in the tuple is the one that was disconnected. + Tuples of the affected asset and the two busbars it previously bridged. """ multi_connected_without_coupler = find_multi_connected_without_coupler(station) if not multi_connected_without_coupler: return station, [] - new_asset_switching_table = np.copy(station.asset_switching_table) + branch_switching_table = np.copy(station.branch_switching_table) + injection_switching_table = np.copy(station.injection_switching_table) diff = [] + n_branch = len(station.branch_connections) for asset_idx, bus1_idx, bus2_idx in multi_connected_without_coupler: - new_asset_switching_table[bus1_idx, asset_idx] = 0 + if asset_idx < n_branch: + branch_switching_table[bus1_idx, asset_idx] = 0 + asset = station.branch_connections[asset_idx].asset + else: + injection_idx = asset_idx - n_branch + injection_switching_table[bus1_idx, injection_idx] = 0 + asset = station.injection_connections[injection_idx].asset diff.append( ( - station.asset_connections[asset_idx].asset, + asset, station.busbars[bus1_idx], station.busbars[bus2_idx], ) ) - return station.model_copy(update={"asset_switching_table": new_asset_switching_table}), diff + return station.model_copy( + update={ + "branch_connections": station.branch_connections, + "injection_connections": station.injection_connections, + "branch_switching_table": branch_switching_table, + "injection_switching_table": injection_switching_table, + "branch_connectivity": station.branch_connectivity, + "injection_connectivity": station.injection_connectivity, + } + ), diff def has_transmission_line_switching(station: MaterializedStation) -> bool: - """Check if the switching table contains transmission line switching + """Check if the switching table contains transmission line switching. Transmission line switching is defined as disconnecting an asset from all busbars on purpose as a remedial action. Out-of-service assets are not considered irrespective of the switching table. Parameters ---------- - station : Station - The station object to check. + station : MaterializedStation + Station to inspect. Returns ------- bool True if the switching table contains transmission line switching, False otherwise. """ - in_service = np.array([asset_connection.asset.in_service for asset_connection in station.asset_connections]) - return np.any((station.asset_switching_table.sum(axis=0) == 0) & in_service).item() + branch_in_service = np.array( + [asset_connection.asset.in_service for asset_connection in station.branch_connections], dtype=bool + ) + injection_in_service = np.array( + [asset_connection.asset.in_service for asset_connection in station.injection_connections], dtype=bool + ) + return bool( + np.any((station.branch_switching_table.sum(axis=0) == 0) & branch_in_service) + or np.any((station.injection_switching_table.sum(axis=0) == 0) & injection_in_service) + ) def find_busbars_for_coupler(busbars: list[Busbar], coupler: BusbarCoupler) -> tuple[Busbar, Busbar]: - """Find the from and two busbars for a coupler + """Find the from-side and to-side busbars for a coupler. - Finds based on the int_id of the busbars. + Matching is based on busbar `int_id` values. Parameters ---------- busbars : list[Busbar] - The list of busbars to search in + Busbars to search. coupler : BusbarCoupler - The coupler to search for + Coupler whose endpoints should be resolved. Returns ------- Busbar - The from busbar + From-side busbar. Busbar - The to busbar + To-side busbar. Raises ------ @@ -620,32 +709,27 @@ def merge_couplers( new: list[BusbarCoupler], busbar_mapping: dict[int, int], ) -> tuple[list[BusbarCoupler], list[BusbarCoupler]]: - """Merge an updated list of couplers into the original list - - Due to preprocessing actions, the new list might contain a subset of the original couplers. - Especially duplicate couplers were removed, so if the original list contains duplicates, both - couplers need to be switched. It assumes that the busbar ids are mapped through the busbar - mapping + """Merge an updated list of couplers into the original list. - If the new list contains duplicates, the couplers are counted as open if all duplicates are open - and as closed otherwise + The processed list may be a subset of the original list because preprocessing can remove + duplicate couplers. If the processed list still contains duplicates, the merged state is open + only if all duplicates are open. Parameters ---------- original : list[BusbarCoupler] - The original list of couplers of the unprocessed station + Original couplers from the unprocessed station. new : list[BusbarCoupler] - The new list of couplers of the processed station after applying the topology + Couplers from the processed station. busbar_mapping : dict[int, int] - The mapping from the original busbar indices to the new busbar indices + Mapping from original busbar ids to the processed station busbar ids. Returns ------- list[BusbarCoupler] - The updated list of couplers + Updated couplers. list[BusbarCoupler] - The coupler diff in this station, i.e. which couplers have been switched. Stores the - new state of the couplers, i.e. which state the coupler has been switched to + Couplers whose open state changed. """ # Store the couplers in a dict for easier access target_state = {} @@ -678,33 +762,28 @@ def merge_stations( new: list[MaterializedStation], missing_station_behavior: Literal["append", "raise"] = "append", ) -> tuple[list[MaterializedStation], list[tuple[str, BusbarCoupler]], list[tuple[str, int, int, bool]]]: - """Merge a list of changed stations into a list of original stations + """Merge a list of changed stations into a list of original stations. - All stations with a grid_model_id that is present in the new list will be updated using - merge_station. The coupler+reassignment diffs will be concatenated. If a new station is not - present in the original list, it will be appended to the end of the list if - missing_station_behavior is "append". + Stations with matching `grid_model_id` values are merged with `merge_station`. + Coupler and reassignment diffs are concatenated across all stations. Parameters ---------- - original : list[Station] - The original list of stations - new : list[Station] - The list of changed stations + original : list[MaterializedStation] + Original stations. + new : list[MaterializedStation] + Updated stations to merge into `original`. missing_station_behavior : Literal["append", "raise"], optional - What to do if a station is not found in the original list, by default "append" + Behavior when a station exists only in `new`. Returns ------- - list[Station] - The updated list of stations + list[MaterializedStation] + Merged station list. list[tuple[str, BusbarCoupler]] - The coupler diff that has been switched, consisting of tuples with the station grid_model_id - and the coupler that has been switched + Coupler diffs as `(station_id, coupler)` tuples. list[tuple[str, int, int, bool]] - The reassignment diff that has been switched, consisting of tuples with the station grid_model_id, - the asset index that was affected, which busbar index was affected and whether the asset was - connected (True) or disconnected (False) to that bus + Asset reassignment diffs as `(station_id, asset_index, busbar_index, connected)` tuples. """ new_stations_found = [] updated_station_list = [] @@ -744,38 +823,33 @@ def merge_stations( def merge_station( original: MaterializedStation, new: MaterializedStation ) -> tuple[MaterializedStation, list[BusbarCoupler], list[tuple[int, int, bool]]]: - """Merge all the changes from the new station into the original station + """Merge all the changes from the new station into the original station. - This will overwrite all assets, couplers and busbars in the original station with the ones from - the new station if the couplers are also found in the new station. Things that are not found in - the new station will be left untouched. Assets that are in the new station but not in the - original will also be left untouched. + Matching assets, couplers, and busbars are updated from `new` when they can be mapped back + onto `original`. Elements that cannot be matched are left unchanged. If all in-service elements of original are also in new, then the returned substation will be electrically equivalent to the new substation. If this is not the case, the returned substation has all possible changes applied, but there are cases in which this is not electrically equivalent to the new substation. - You can use asset_topology_helpers.compare_stations to check which elements are different - in the two stations and infer the differences. + Use `compare_stations` to inspect structural differences between both stations. Parameters ---------- - original : Station - The original station that should be modified - new : Station - The new station that contains the changes that should be merged into the original station + original : MaterializedStation + Original station to update. + new : MaterializedStation + Station that provides the target changes. Returns ------- - Station - The modified original station, with all the changes from the new station merged in + MaterializedStation + Copy of `original` with all mergeable changes from `new` applied. list[BusbarCoupler] - The coupler diff that has been switched + Couplers whose open state changed. list[tuple[int, int, bool]] - The asset diff that has been switched. Each tuple contains the asset index that was - affected, which busbar index was affected and whether the asset was connected (True) or - disconnected (False) to that bus + Asset switching diff as `(asset_index, busbar_index, connected)` tuples. """ if original == new: return original, [], [] @@ -785,14 +859,22 @@ def merge_station( busbar_mapping = {} busbar_int_id_mapping = {} max_busbar_id = max(busbar.int_id for busbar in new.busbars) - asset_mapping, new_couplers, coupler_diff = map_busbars_and_assets( + branch_asset_mapping, injection_asset_mapping, new_couplers, coupler_diff = map_busbars_and_assets( original, new, busbar_mapping, busbar_int_id_mapping, max_busbar_id ) # Loop through the switching table and copy the values over for which there is a mapping - new_asset_switching_table = original.asset_switching_table.copy() + new_branch_switching_table = original.branch_switching_table.copy() + new_injection_switching_table = original.injection_switching_table.copy() station, asset_diff = update_asset_switching_table( - original, new, busbar_mapping, asset_mapping, new_couplers, new_asset_switching_table + original, + new, + busbar_mapping, + branch_asset_mapping, + injection_asset_mapping, + new_couplers, + new_branch_switching_table, + new_injection_switching_table, ) return station, coupler_diff, asset_diff @@ -801,66 +883,88 @@ def update_asset_switching_table( original_station: MaterializedStation, new_station: MaterializedStation, busbar_mapping: dict[int, int], - asset_mapping: dict[int, int], + branch_asset_mapping: dict[int, int], + injection_asset_mapping: dict[int, int], new_couplers: list[BusbarCoupler], - new_asset_switching_table: np.ndarray, + new_branch_switching_table: np.ndarray, + new_injection_switching_table: np.ndarray, ) -> tuple[MaterializedStation, list[tuple[int, int, bool]]]: - """Update the asset switching table of the original station with the values from the new station. + """Update switching tables and couplers using a mapped target station. - This is a separate function to reduce the complexity of merge_station. - It also updates the couplers in the original station with the new couplers. + This helper copies branch and injection switching states from `new_station` into + `original_station` for all mapped busbars and assets. It also replaces the + coupler list with `new_couplers` and records the resulting switching diff. Parameters ---------- - original_station : Station - The original station that should be modified - new_station : Station - The new station that contains the changes that should be merged into the original station + original_station : MaterializedStation + Original station that receives the mapped switching updates. + new_station : MaterializedStation + Station that provides the target switching states. busbar_mapping : dict[int, int] - The mapping from the original busbar indices to the new busbar indices - asset_mapping : dict[int, int] - The mapping from the original asset indices to the new asset indices + Mapping from original busbar indices to busbar indices in `new_station`. + branch_asset_mapping : dict[int, int] + Mapping from original branch connection indices to branch indices in `new_station`. + injection_asset_mapping : dict[int, int] + Mapping from original injection connection indices to injection indices in `new_station`. new_couplers : list[BusbarCoupler] - The new list of couplers that should be merged into the original station - new_asset_switching_table : np.ndarray - The new asset switching table that should be updated with the values from the new station. This - is modified in place and returned as part of the new station object. + Couplers to store on the returned station. + new_branch_switching_table : np.ndarray + Branch switching table buffer for the returned station. Modified in place. + new_injection_switching_table : np.ndarray + Injection switching table buffer for the returned station. Modified in place. Returns ------- - Station - The modified original station, with the updated asset switching table and couplers + MaterializedStation + Copy of `original_station` with updated switching tables and couplers. list[tuple[int, int, bool]] - The asset diff that has been switched. Each tuple contains the asset index that was - affected, which busbar index was affected and whether the asset was connected (True) or - disconnected (False) to that bus + Switching diff as `(asset_index, busbar_index, connected)` tuples. """ asset_diff = [] - for busbar_idx in range(original_station.asset_switching_table.shape[0]): - if busbar_idx in busbar_mapping: - mapped_busbar_idx = busbar_mapping[busbar_idx] - for asset_idx in range(original_station.asset_switching_table.shape[1]): - if asset_idx in asset_mapping: - mapped_asset_idx = asset_mapping[asset_idx] - - if ( - new_asset_switching_table[busbar_idx, asset_idx] - != new_station.asset_switching_table[mapped_busbar_idx, mapped_asset_idx] - ): - asset_diff.append( - ( - asset_idx, - busbar_idx, - bool(new_station.asset_switching_table[mapped_busbar_idx, mapped_asset_idx]), - ) - ) - - new_asset_switching_table[busbar_idx, asset_idx] = new_station.asset_switching_table[ - mapped_busbar_idx, mapped_asset_idx - ] + for busbar_idx, mapped_busbar_idx in busbar_mapping.items(): + for branch_idx, mapped_branch_idx in branch_asset_mapping.items(): + if ( + new_branch_switching_table[busbar_idx, branch_idx] + != new_station.branch_switching_table[mapped_busbar_idx, mapped_branch_idx] + ): + asset_diff.append( + ( + branch_idx, + busbar_idx, + bool(new_station.branch_switching_table[mapped_busbar_idx, mapped_branch_idx]), + ) + ) + new_branch_switching_table[busbar_idx, branch_idx] = new_station.branch_switching_table[ + mapped_busbar_idx, mapped_branch_idx + ] + + for injection_idx, mapped_injection_idx in injection_asset_mapping.items(): + if ( + new_injection_switching_table[busbar_idx, injection_idx] + != new_station.injection_switching_table[mapped_busbar_idx, mapped_injection_idx] + ): + asset_diff.append( + ( + injection_idx + len(original_station.branch_connections), + busbar_idx, + bool(new_station.injection_switching_table[mapped_busbar_idx, mapped_injection_idx]), + ) + ) + new_injection_switching_table[busbar_idx, injection_idx] = new_station.injection_switching_table[ + mapped_busbar_idx, mapped_injection_idx + ] original_station = original_station.model_copy( - update={"couplers": new_couplers, "asset_switching_table": new_asset_switching_table} + update={ + "couplers": new_couplers, + "branch_connections": original_station.branch_connections, + "injection_connections": original_station.injection_connections, + "branch_switching_table": new_branch_switching_table, + "injection_switching_table": new_injection_switching_table, + "branch_connectivity": original_station.branch_connectivity, + "injection_connectivity": original_station.injection_connectivity, + } ) return original_station, asset_diff @@ -871,67 +975,104 @@ def map_busbars_and_assets( busbar_mapping: dict[int, int], busbar_int_id_mapping: dict[int, int], max_busbar_id: int, -) -> tuple[dict[int, int], list[BusbarCoupler], list[BusbarCoupler]]: - """Find a mapping from old busbars to new busbars and from old assets to new assets +) -> tuple[dict[int, int], dict[int, int], list[BusbarCoupler], list[BusbarCoupler]]: + """Build busbar and asset mappings between two materialized stations. - This is a separate function to reduce the complexity of merge_station. - It also merges the couplers and returns the new couplers and the coupler diff. + This helper matches busbars and asset connections by `grid_model_id`, updates the + provided busbar mapping dictionaries in place, and prepares merged coupler state + for `merge_station`. Parameters ---------- - original_station : Station - The original station that should be modified - new_station : Station - The new station that contains the changes that should be merged into the original station + original_station : MaterializedStation + Original station whose busbars and asset connections should be matched. + new_station : MaterializedStation + Target station that provides the reference busbars, assets, and couplers. busbar_mapping : dict[int, int] - The mapping from the original busbar indices to the new busbar indices. This is modified in place and - returned as part of the new station object. + Mapping from original busbar indices to busbar indices in `new_station`. + Modified in place. busbar_int_id_mapping : dict[int, int] - The mapping from the original busbar int_ids to the new busbar int_ids. This is modified in place and - returned as part of the new station object. + Mapping from original busbar `int_id` values to corresponding `int_id` values + in `new_station`. Missing busbars are assigned synthetic ids beyond + `max_busbar_id`. Modified in place. max_busbar_id : int - The maximum busbar int_id in the new station. - This is used to assign new int_ids to busbars that are in the original station but not in the new station, - to avoid conflicts with existing busbars in the new station. + Maximum busbar `int_id` present in `new_station`. Returns ------- dict[int, int] - The mapping from the original asset indices to the new asset indices. This is modified in place - and returned as part of the new station object. + Mapping from original branch connection indices to branch indices in `new_station`. + dict[int, int] + Mapping from original injection connection indices to injection indices in `new_station`. list[BusbarCoupler] - The new list of couplers that should be merged into the original station + Coupler list to apply to the original station. list[BusbarCoupler] - The coupler diff in this station, i.e. which couplers have been switched. Stores the - new state of the couplers, i.e. which state the coupler has been switched to + Couplers whose open state changes during the merge. """ - for i, busbar in enumerate(original_station.busbars): - found = False - for j, other in enumerate(new_station.busbars): - if busbar.grid_model_id == other.grid_model_id: - busbar_mapping[i] = j - busbar_int_id_mapping[busbar.int_id] = other.int_id - found = True - break - if not found: + + def _build_index_by_grid_model_id(elements: list[Union[Busbar, SwitchableAsset]]) -> dict[str, int]: + """Map each element grid model id to its position in the input list. + + Parameters + ---------- + elements : list[Union[Busbar, SwitchableAsset]] + Elements that expose a `grid_model_id` attribute. + + Returns + ------- + dict[str, int] + Mapping from `grid_model_id` to element position. + """ + return {element.grid_model_id: index for index, element in enumerate(elements)} + + def _map_asset_connections( + original_connections: list[MaterializedAssetConnection], + new_connections: list[MaterializedAssetConnection], + ) -> dict[int, int]: + """Map original asset connection indices to matching target connection indices. + + Parameters + ---------- + original_connections : list[MaterializedAssetConnection] + Original asset connections to map. + new_connections : list[MaterializedAssetConnection] + Target asset connections that define the mapped indices. + + Returns + ------- + dict[int, int] + Mapping from original connection index to target connection index. + """ + new_asset_indices = _build_index_by_grid_model_id([asset_connection.asset for asset_connection in new_connections]) + return { + index: new_asset_indices[asset_connection.asset.grid_model_id] + for index, asset_connection in enumerate(original_connections) + if asset_connection.asset.grid_model_id in new_asset_indices + } + + new_busbar_indices = _build_index_by_grid_model_id(new_station.busbars) + new_busbar_int_ids = {busbar.grid_model_id: busbar.int_id for busbar in new_station.busbars} + for index, busbar in enumerate(original_station.busbars): + mapped_busbar_index = new_busbar_indices.get(busbar.grid_model_id) + if mapped_busbar_index is None: # Make sure to not accidentally map to an existing busbar busbar_int_id_mapping[busbar.int_id] = busbar.int_id + max_busbar_id + 1 + continue - # Same for the switchable assets - asset_mapping = {} - for i, asset_connection in enumerate(original_station.asset_connections): - asset = asset_connection.asset - for j, other_connection in enumerate(new_station.asset_connections): - other = other_connection.asset - if asset.grid_model_id == other.grid_model_id: - asset_mapping[i] = j - break + busbar_mapping[index] = mapped_busbar_index + busbar_int_id_mapping[busbar.int_id] = new_busbar_int_ids[busbar.grid_model_id] + + branch_asset_mapping = _map_asset_connections(original_station.branch_connections, new_station.branch_connections) + injection_asset_mapping = _map_asset_connections( + original_station.injection_connections, + new_station.injection_connections, + ) # Merge couplers new_couplers, coupler_diff = merge_couplers( original_station.couplers, new_station.couplers, busbar_mapping=busbar_int_id_mapping ) - return asset_mapping, new_couplers, coupler_diff + return branch_asset_mapping, injection_asset_mapping, new_couplers, coupler_diff def compare_stations( @@ -944,7 +1085,7 @@ def compare_stations( list[SwitchableAsset], list[SwitchableAsset], ]: - """Compare two stations and return the missing elements + """Compare two stations and return the missing elements. It uses grid_model_ids to compare the assets, busbars and couplers. It does not consider different switching states or coupler states, but just checks if all the elements are also in @@ -952,25 +1093,25 @@ def compare_stations( Parameters ---------- - a : Station - The first station to compare. - b : Station - The second station to compare. + a : MaterializedStation + First station to compare. + b : MaterializedStation + Second station to compare. Returns ------- list[BusbarCoupler] - The couplers that are in a but not in b. + Couplers present in `a` but not in `b`. list[BusbarCoupler] - The couplers that are in b but not in a. + Couplers present in `b` but not in `a`. list[Busbar] - The busbars that are in a but not in b. + Busbars present in `a` but not in `b`. list[Busbar] - The busbars that are in b but not in a. + Busbars present in `b` but not in `a`. list[SwitchableAsset] - The assets that are in a but not in b. + Assets present in `a` but not in `b`. list[SwitchableAsset] - The assets that are in b but not in a. + Assets present in `b` but not in `a`. """ a_busbars = set(busbar.grid_model_id for busbar in a.busbars) b_busbars = set(busbar.grid_model_id for busbar in b.busbars) @@ -978,8 +1119,14 @@ def compare_stations( a_couplers = set(coupler.grid_model_id for coupler in a.couplers) b_couplers = set(coupler.grid_model_id for coupler in b.couplers) - a_assets = set(asset_connection.asset.grid_model_id for asset_connection in a.asset_connections) - b_assets = set(asset_connection.asset.grid_model_id for asset_connection in b.asset_connections) + a_assets = { + *(asset_connection.asset.grid_model_id for asset_connection in a.branch_connections), + *(asset_connection.asset.grid_model_id for asset_connection in a.injection_connections), + } + b_assets = { + *(asset_connection.asset.grid_model_id for asset_connection in b.branch_connections), + *(asset_connection.asset.grid_model_id for asset_connection in b.injection_connections), + } return ( [coupler for coupler in a.couplers if coupler.grid_model_id not in b_couplers], @@ -987,14 +1134,28 @@ def compare_stations( [busbar for busbar in a.busbars if busbar.grid_model_id not in b_busbars], [busbar for busbar in b.busbars if busbar.grid_model_id not in a_busbars], [ - asset_connection.asset - for asset_connection in a.asset_connections - if asset_connection.asset.grid_model_id not in b_assets + *( + asset_connection.asset + for asset_connection in a.branch_connections + if asset_connection.asset.grid_model_id not in b_assets + ), + *( + asset_connection.asset + for asset_connection in a.injection_connections + if asset_connection.asset.grid_model_id not in b_assets + ), ], [ - asset_connection.asset - for asset_connection in b.asset_connections - if asset_connection.asset.grid_model_id not in a_assets + *( + asset_connection.asset + for asset_connection in b.branch_connections + if asset_connection.asset.grid_model_id not in a_assets + ), + *( + asset_connection.asset + for asset_connection in b.injection_connections + if asset_connection.asset.grid_model_id not in a_assets + ), ], ) @@ -1008,9 +1169,9 @@ def load_asset_topology_fs( Parameters ---------- filesystem : AbstractFileSystem - The file system to use to load the asset topology. + File system to load from. file_path : Union[str, Path] - The path to the file containing the asset topology in json format. + Path to the JSON file containing the topology. Returns ------- @@ -1025,17 +1186,17 @@ def load_asset_topology_fs( def load_asset_topology(filename: Union[str, Path]) -> Topology: - """Load an asset topology from a file + """Load an asset topology from a file. Parameters ---------- filename : Union[str, Path] - The filename to load the asset topology from + File name to load the topology from. Returns ------- Topology - The loaded asset topology + Loaded topology. """ return load_asset_topology_fs( filesystem=LocalFileSystem(), @@ -1044,118 +1205,97 @@ def load_asset_topology(filename: Union[str, Path]) -> Topology: def save_asset_topology_fs(filesystem: AbstractFileSystem, filename: Union[str, Path], asset_topology: Topology) -> None: - """Save an asset topology to a file system + """Save an asset topology to a file system. Parameters ---------- filesystem : AbstractFileSystem - The file system to save the asset topology to + File system to save to. filename : Union[str, Path] - The filename to save the asset topology to - asset_topology: Topology - The asset topology to save + File name to save the topology to. + asset_topology : Topology + Topology to save. """ with filesystem.open(str(filename), "w", encoding="utf-8") as file: file.write(asset_topology.model_dump_json(indent=2)) def save_asset_topology(filename: Union[str, Path], asset_topology: Topology) -> None: - """Save an asset topology to a file + """Save an asset topology to a file. Parameters ---------- filename : Union[str, Path] - The filename to save the asset topology to - asset_topology: Topology - The asset topology to save + File name to save the topology to. + asset_topology : Topology + Topology to save. """ save_asset_topology_fs(LocalFileSystem(), filename, asset_topology) -def get_connected_assets( - station: Union[MaterializedStation, RawStation], - busbar_index: int, - topology_assets: Optional[list[SwitchableAsset]] = None, -) -> list[SwitchableAsset]: - """ - Get the assets connected to a specific busbar in a station. - - Parameters - ---------- - station : MaterializedStation | RawStation - The station object containing the switching table and assets. - busbar_index : int - The index of the busbar in the switching table. - topology_assets : Optional[list[SwitchableAsset]] - Topology-owned assets used to resolve ``RawStation.asset_connections``. Required when ``station`` is a - ``RawStation``. - - Returns - ------- - list of SwitchableAsset - A list of assets connected to the specified busbar. - """ - connected_asset_indices = np.nonzero(station.asset_switching_table[busbar_index])[0] - if isinstance(station, MaterializedStation): - return [ - station.asset_connections[i].asset - for i in connected_asset_indices - if station.asset_connections[i].asset.in_service - ] - - if topology_assets is None: - raise ValueError("topology_assets must be provided when resolving connected assets for a RawStation") - - asset_map = {asset.grid_model_id: asset for asset in topology_assets} - return [ - asset_map[station.asset_connections[i].asset_id] - for i in connected_asset_indices - if asset_map[station.asset_connections[i].asset_id].in_service - ] - - def accumulate_diffs( realized_stations: list[AppliedStation], ) -> tuple[ list[tuple[str, BusbarCoupler]], list[tuple[str, int, int, bool]], + list[tuple[str, int, int, bool]], + list[tuple[str, int]], list[tuple[str, int]], ]: - """Accumulate the diffs of the realized stations into the format of realized topology + """Accumulate the diffs of the realized stations into the format of realized topology. Parameters ---------- realized_stations : list[AppliedStation] - The realized stations to accumulate + Realized stations to accumulate. Returns ------- list[tuple[str, BusbarCoupler]] - The accumulated coupler diff + Accumulated coupler diff. list[tuple[str, int, int, bool]] - The accumulated reassignment diff + Accumulated branch reassignment diff. + list[tuple[str, int, int, bool]] + Accumulated injection reassignment diff. + list[tuple[str, int]] + Accumulated branch disconnection diff. list[tuple[str, int]] - The accumulated disconnection diff + Accumulated injection disconnection diff. """ coupler_diff = [] - reassignment_diff = [] - disconnection_diff = [] + branch_reassignment_diff = [] + injection_reassignment_diff = [] + branch_disconnection_diff = [] + injection_disconnection_diff = [] for station in realized_stations: s_id = station.station.grid_model_id coupler_diff.extend([(s_id, coupler) for coupler in station.coupler_diff]) - reassignment_diff.extend( - [(s_id, asset_idx, bus_idx, connected) for (asset_idx, bus_idx, connected) in station.reassignment_diff] + branch_reassignment_diff.extend( + [(s_id, asset_idx, bus_idx, connected) for (asset_idx, bus_idx, connected) in station.branch_reassignment_diff] ) - disconnection_diff.extend([(s_id, asset_idx) for asset_idx in station.disconnection_diff]) + injection_reassignment_diff.extend( + [ + (s_id, asset_idx, bus_idx, connected) + for (asset_idx, bus_idx, connected) in station.injection_reassignment_diff + ] + ) + branch_disconnection_diff.extend([(s_id, asset_idx) for asset_idx in station.branch_disconnection_diff]) + injection_disconnection_diff.extend([(s_id, asset_idx) for asset_idx in station.injection_disconnection_diff]) - return coupler_diff, reassignment_diff, disconnection_diff + return ( + coupler_diff, + branch_reassignment_diff, + injection_reassignment_diff, + branch_disconnection_diff, + injection_disconnection_diff, + ) def station_diff( start_station: MaterializedStation, target_station: MaterializedStation, ) -> AppliedStation: - """Compute the diff between two stations + """Compute the diff between two stations. The same station must be described by both inputs, i.e. the assets, busbars and couplers (except for their open state) must be the same. @@ -1163,18 +1303,21 @@ def station_diff( Parameters ---------- start_station : MaterializedStation - The starting station from which to start the diff + Starting station. target_station : MaterializedStation - The ending station to which the diff shall lead when applied to the starting station + Target station. Returns ------- AppliedStation - The realized station containing the target station and the coupler, reassignment and disconnection diffs + Realized station containing the target station and all derived diffs. """ - assert [s.asset.grid_model_id for s in start_station.asset_connections] == [ - s.asset.grid_model_id for s in target_station.asset_connections - ], "Assets do not match" + assert [s.asset.grid_model_id for s in start_station.branch_connections] == [ + s.asset.grid_model_id for s in target_station.branch_connections + ], "Branch assets do not match" + assert [s.asset.grid_model_id for s in start_station.injection_connections] == [ + s.asset.grid_model_id for s in target_station.injection_connections + ], "Injection assets do not match" assert [b.grid_model_id for b in start_station.busbars] == [b.grid_model_id for b in target_station.busbars], ( "Busbars do not match" ) @@ -1182,36 +1325,64 @@ def station_diff( "Couplers do not match" ) - reassignment_diff = [] - disconnection_diff = [] - for asset_index in range(len(start_station.asset_connections)): - target_disconnected = ~np.any(target_station.asset_switching_table[:, asset_index]) - start_disconnected = ~np.any(start_station.asset_switching_table[:, asset_index]) - - if start_disconnected and not target_disconnected: - raise NotImplementedError( - "Reconnections are not supported yet, there is no diff for that" - + f" (asset {asset_index} in station {start_station.grid_model_id})" - ) + def _table_diff( + start_table: np.ndarray, + target_table: np.ndarray, + asset_kind: str, + ) -> tuple[list[tuple[int, int, bool]], list[int]]: + """Compute reassignment and disconnection diffs for one asset table. + + Parameters + ---------- + start_table : np.ndarray + Initial switching table. + target_table : np.ndarray + Target switching table. + asset_kind : str + Asset kind used in error messages. + + Returns + ------- + list[tuple[int, int, bool]] + Reassignment diff as `(asset_index, busbar_index, connected)` tuples. + list[int] + Asset indices that become disconnected. + """ + reassignment_diff_local: list[tuple[int, int, bool]] = [] + disconnection_diff_local: list[int] = [] + for asset_index in range(start_table.shape[1]): + target_disconnected = ~np.any(target_table[:, asset_index]) + start_disconnected = ~np.any(start_table[:, asset_index]) + + if start_disconnected and not target_disconnected: + raise NotImplementedError( + "Reconnections are not supported yet, there is no diff for that" + + f" ({asset_kind} {asset_index} in station {start_station.grid_model_id})" + ) - if target_disconnected and not start_disconnected: - # if the asset is connected in the target station but disconnected in the starting station - # we need to add it to the diff - disconnection_diff.append(asset_index) + if target_disconnected and not start_disconnected: + disconnection_diff_local.append(int(asset_index)) - # No reassignment diff if the asset is disconnected - if target_disconnected: - continue + if target_disconnected: + continue - xor_diff = np.logical_xor( - start_station.asset_switching_table[:, asset_index], - target_station.asset_switching_table[:, asset_index], - ) - for busbar_index in np.flatnonzero(xor_diff): - # If the start and end switching entries differ, add it to the reassignment diff - reassignment_diff.append( - (asset_index, busbar_index, target_station.asset_switching_table[busbar_index, asset_index]) - ) + xor_diff = np.logical_xor(start_table[:, asset_index], target_table[:, asset_index]) + for busbar_index in np.flatnonzero(xor_diff): + reassignment_diff_local.append( + (int(asset_index), int(busbar_index), bool(target_table[busbar_index, asset_index])) + ) + return reassignment_diff_local, disconnection_diff_local + + branch_reassignment_diff, branch_disconnection_diff = _table_diff( + start_station.branch_switching_table, + target_station.branch_switching_table, + "branch", + ) + injection_reassignment_diff, injection_disconnection_diff = _table_diff( + start_station.injection_switching_table, + target_station.injection_switching_table, + "injection", + ) coupler_diff = [] for start_coupler, target_coupler in zip(start_station.couplers, target_station.couplers, strict=True): @@ -1221,8 +1392,10 @@ def station_diff( return AppliedStation( station=target_station, coupler_diff=coupler_diff, - reassignment_diff=reassignment_diff, - disconnection_diff=disconnection_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ) @@ -1230,20 +1403,19 @@ def topology_diff( start_topo: Topology, target_topo: Topology, ) -> RealizedTopology: - """Compute the difference between two topologies + """Compute the difference between two topologies. Parameters ---------- start_topo : Topology - The starting topology + Starting topology. target_topo : Topology - The targeted topology. + Target topology. Returns ------- RealizedTopology - The realized topology containing the target topology and the coupler, reassignment and disconnection diffs that lead - from the starting topology to the target topology. + Realized topology containing the target topology and all diffs from the start topology. """ realized_stations = [ station_diff(start_station, target_station) @@ -1251,67 +1423,96 @@ def topology_diff( start_topo.materialize_stations(), target_topo.materialize_stations(), strict=True ) ] - coupler_diff, reassignment_diff, disconnection_diff = accumulate_diffs(realized_stations) + ( + coupler_diff, + branch_reassignment_diff, + injection_reassignment_diff, + branch_disconnection_diff, + injection_disconnection_diff, + ) = accumulate_diffs(realized_stations) return RealizedTopology( topology=target_topo, coupler_diff=coupler_diff, - reassignment_diff=reassignment_diff, - disconnection_diff=disconnection_diff, + branch_reassignment_diff=branch_reassignment_diff, + injection_reassignment_diff=injection_reassignment_diff, + branch_disconnection_diff=branch_disconnection_diff, + injection_disconnection_diff=injection_disconnection_diff, ) def order_station_assets( station: MaterializedStation, asset_ids: list[str] ) -> tuple[MaterializedStation, list[str], list[str]]: - """Orders the assets in a station according to a list of asset ids. + """Order station assets according to a list of asset ids. - If an asset is not found in the station, it will be added to the not_found list. - If an asset is not present in the asset_ids list, it will be dropped from the station. + Asset ids not present in the station are reported in `not_found`. + Assets omitted from `asset_ids` are dropped and reported in `ignored`. Parameters ---------- - station : Station - The station to order. + station : MaterializedStation + Station to reorder. asset_ids : list[str] - A list of asset ids. The assets will be ordered according to the grid_model_id. + Asset ids in the desired order. Returns ------- - Station - The ordered station + MaterializedStation + Station with reordered assets. list[str] - A list of asset ids that were not found in the station + Asset ids that were not found in the station. list[str] - A list of asset ids that were ignored, i.e. not present in the asset_ids list + Asset ids ignored because they were not present in `asset_ids`. """ - new_asset_connections = [] + branch_lookup = { + asset_connection.asset.grid_model_id: (index, asset_connection) + for index, asset_connection in enumerate(station.branch_connections) + } + injection_lookup = { + asset_connection.asset.grid_model_id: (index, asset_connection) + for index, asset_connection in enumerate(station.injection_connections) + } + new_branch_connections = [] + new_injection_connections = [] not_found = [] - old_positions = [] + branch_positions = [] + injection_positions = [] for asset_id in asset_ids: - found = False - for pos, asset_connection in enumerate(station.asset_connections): - asset = asset_connection.asset - if asset.grid_model_id == asset_id: - new_asset_connections.append(asset_connection) - old_positions.append(pos) - found = True - break - if not found: + if asset_id in branch_lookup: + pos, asset_connection = branch_lookup[asset_id] + new_branch_connections.append(asset_connection) + branch_positions.append(pos) + elif asset_id in injection_lookup: + pos, asset_connection = injection_lookup[asset_id] + new_injection_connections.append(asset_connection) + injection_positions.append(pos) + else: not_found.append(asset_id) ignored = [ asset_connection.asset.grid_model_id - for index, asset_connection in enumerate(station.asset_connections) - if index not in old_positions + for index, asset_connection in enumerate(station.branch_connections) + if index not in branch_positions + ] + [ + asset_connection.asset.grid_model_id + for index, asset_connection in enumerate(station.injection_connections) + if index not in injection_positions ] - asset_switching_table = station.asset_switching_table[:, old_positions] - asset_connectivity = station.asset_connectivity[:, old_positions] if station.asset_connectivity is not None else None station = station.model_copy( update={ - "asset_connections": new_asset_connections, - "asset_switching_table": asset_switching_table, - "asset_connectivity": asset_connectivity, + "branch_connections": new_branch_connections, + "injection_connections": new_injection_connections, + "branch_switching_table": station.branch_switching_table[:, branch_positions], + "injection_switching_table": station.injection_switching_table[:, injection_positions], + "branch_connectivity": ( + station.branch_connectivity[:, branch_positions] if station.branch_connectivity is not None else None + ), + "injection_connectivity": ( + station.injection_connectivity[:, injection_positions] + if station.injection_connectivity is not None + else None + ), } ) MaterializedStation.model_validate(station) @@ -1319,24 +1520,24 @@ def order_station_assets( def order_topology(topology: Topology, station_ids: list[str]) -> tuple[Topology, list[str]]: - """Orders the stations in a topology according to a list of ids. + """Order topology stations according to a list of ids. - If a station is not found in the topology, it will be added to the not_found list. - If a station is not present in the station_ids list, it will be dropped. + Station ids not present in the topology are reported in `not_found`. + Stations omitted from `station_ids` are dropped. Parameters ---------- topology : Topology - The topology to order. + Topology to reorder. station_ids : list[str] - A list of station ids. The stations will be ordered according to the grid_model_id. + Station ids in the desired order. Returns ------- Topology - The ordered topology + Topology with reordered stations. list[str] - A list of station ids that were not found in the topology + Station ids that were not found in the topology. """ new_stations = [] not_found = [] @@ -1350,124 +1551,315 @@ def order_topology(topology: Topology, station_ids: list[str]) -> tuple[Topology if not found: not_found.append(relevant_node) - topology = copy_topology_with_updates(topology, new_stations, topology.assets, topology.asset_bays) + topology = copy_topology_with_updates( + topology, + new_stations, + topology.asset_bays, + branch_assets=topology.branch_assets, + injection_assets=topology.injection_assets, + ) return topology, not_found -def fuse_coupler( - station: MaterializedStation, - coupler_grid_model_id: str, - copy_info_from: bool = True, -) -> MaterializedStation: - """Fuses a coupler by merging the adjacent busbars into one busbar. +def _coupler_connects_same_busbars(coupler: BusbarCoupler, other_coupler: BusbarCoupler) -> bool: + """Return whether two couplers connect the same busbar pair. Parameters ---------- - station : Station - The station with the coupler to fuse. Assumes that the coupler is in the station. The open/closed state of the - coupler is disregarded, i.e. the coupler will be fused regardless of its state. - coupler_grid_model_id : str - The grid_model_id of the coupler to fuse - copy_info_from : bool, optional - Whether the new busbar retains the information (grid_model_id, etc) of the busbar on the from side of the coupler, - by default True. If False, the busbar on the to side of the coupler is used. + coupler : BusbarCoupler + Reference coupler. + other_coupler : BusbarCoupler + Coupler to compare against `coupler`. Returns ------- - Station - The station with the coupler fused. The coupler is removed and the busbars are merged. + bool + `True` if both couplers connect the same busbar pair. """ - coupler = next((c for c in station.couplers if c.grid_model_id == coupler_grid_model_id), None) - if coupler is None: - raise ValueError(f"Coupler {coupler_grid_model_id} not found in station {station.grid_model_id}") + return ( + other_coupler.busbar_from_id == coupler.busbar_from_id and other_coupler.busbar_to_id == coupler.busbar_to_id + ) or (other_coupler.busbar_to_id == coupler.busbar_from_id and other_coupler.busbar_from_id == coupler.busbar_to_id) - busbar_from_index = next((index for index, b in enumerate(station.busbars) if b.int_id == coupler.busbar_from_id), None) - busbar_to_index = next((index for index, b in enumerate(station.busbars) if b.int_id == coupler.busbar_to_id), None) - assert busbar_from_index is not None, f"Busbar {coupler.busbar_from_id} not found in station {station.grid_model_id}" - assert busbar_to_index is not None, f"Busbar {coupler.busbar_to_id} not found in station {station.grid_model_id}" +def _validate_coupler_can_be_fused(station: MaterializedStation, coupler: BusbarCoupler) -> None: + """Raise if the coupler has a parallel coupler on the same busbar pair. - if ( - len( - [ - c - for c in station.couplers - if (c.busbar_from_id == coupler.busbar_from_id and c.busbar_to_id == coupler.busbar_to_id) - or (c.busbar_to_id == coupler.busbar_from_id and c.busbar_from_id == coupler.busbar_to_id) - ] - ) - > 1 - ): + Parameters + ---------- + station : MaterializedStation + Station containing the coupler. + coupler : BusbarCoupler + Coupler that should be fused. + + Raises + ------ + ValueError + If more than one coupler connects the same busbar pair. + """ + parallel_couplers = [ + other_coupler for other_coupler in station.couplers if _coupler_connects_same_busbars(coupler, other_coupler) + ] + if len(parallel_couplers) > 1: raise ValueError( - f"Coupler {coupler_grid_model_id} has parallel couplers in station {station.grid_model_id}, " + f"Coupler {coupler.grid_model_id} has parallel couplers in station {station.grid_model_id}, " "cannot fuse parallel couplers with the same busbars" ) - switching_row = station.asset_switching_table[busbar_from_index] | station.asset_switching_table[busbar_to_index] - connectivity_row = station.asset_connectivity[busbar_from_index] | station.asset_connectivity[busbar_to_index] - # Remove either the from or the to busbar - busbar_index_to_remove = busbar_to_index if copy_info_from else busbar_from_index - busbar_index_to_keep = busbar_from_index if copy_info_from else busbar_to_index +def _resolve_fused_busbars( + station: MaterializedStation, + coupler: BusbarCoupler, + copy_info_from: bool, +) -> tuple[int, int, int, int, Busbar, Busbar]: + """Resolve busbar indices and busbar objects used during coupler fusion. + + Parameters + ---------- + station : MaterializedStation + Station containing the coupler. + coupler : BusbarCoupler + Coupler whose adjacent busbars should be resolved. + copy_info_from : bool + Whether metadata should be kept from the from-side busbar. + + Returns + ------- + int + From-side busbar index. + int + To-side busbar index. + int + Kept busbar index. + int + Removed busbar index. + Busbar + Kept busbar. + Busbar + Removed busbar. + """ + busbar_from, busbar_to = find_busbars_for_coupler(station.busbars, coupler) + busbar_index_by_int_id = {busbar.int_id: index for index, busbar in enumerate(station.busbars)} + busbar_from_index = busbar_index_by_int_id[busbar_from.int_id] + busbar_to_index = busbar_index_by_int_id[busbar_to.int_id] + keep_busbar_index = busbar_from_index if copy_info_from else busbar_to_index + remove_busbar_index = busbar_to_index if copy_info_from else busbar_from_index + keep_busbar = station.busbars[keep_busbar_index] + remove_busbar = station.busbars[remove_busbar_index] + return busbar_from_index, busbar_to_index, keep_busbar_index, remove_busbar_index, keep_busbar, remove_busbar + + +def _merge_and_delete_fused_busbar_row( + table: Optional[np.ndarray], + busbar_from_index: int, + busbar_to_index: int, + keep_busbar_index: int, + remove_busbar_index: int, +) -> Optional[np.ndarray]: + """Merge fused busbar rows and delete the removed busbar row. + + Parameters + ---------- + table : Optional[np.ndarray] + Switching or connectivity table to update. + busbar_from_index : int + From-side busbar index. + busbar_to_index : int + To-side busbar index. + keep_busbar_index : int + Busbar index that should remain in the table. + remove_busbar_index : int + Busbar index that should be removed from the table. + + Returns + ------- + Optional[np.ndarray] + Updated table, or `None` if the input table is `None`. + """ + if table is None: + return None + merged_table = np.copy(table) + merged_table[keep_busbar_index] = merged_table[busbar_from_index] | merged_table[busbar_to_index] + return np.delete(merged_table, remove_busbar_index, axis=0) - new_switching_table = np.copy(station.asset_switching_table) - new_switching_table[busbar_index_to_keep] = switching_row - new_switching_table = np.delete(new_switching_table, busbar_index_to_remove, axis=0) - if station.asset_connectivity is not None: - new_connectivity_table = np.copy(station.asset_connectivity) - new_connectivity_table[busbar_index_to_keep] = connectivity_row - new_connectivity_table = np.delete(new_connectivity_table, busbar_index_to_remove, axis=0) - else: - new_connectivity_table = None +def _replace_sr_keys_for_fused_busbar( + asset_bay: Optional[AssetBay], + keep_busbar: Busbar, + remove_busbar: Busbar, +) -> Optional[AssetBay]: + """Rewrite SR switch busbar keys from the removed busbar to the kept one. - busbar_to_remove = station.busbars[busbar_index_to_remove] - busbar_to_keep = station.busbars[busbar_index_to_keep] + Parameters + ---------- + asset_bay : Optional[AssetBay] + Asset bay whose SR switch mapping should be updated. + keep_busbar : Busbar + Busbar that remains after fusion. + remove_busbar : Busbar + Busbar that is removed during fusion. - def _replace_sr_keys(asset_bay: Optional[AssetBay]) -> Optional[AssetBay]: - """Update the sr switch asset bay if it is connected to the removed busbar.""" - if asset_bay is None: - return None - if ( - busbar_to_remove.grid_model_id in asset_bay.sr_switch_grid_model_id.keys() - and busbar_to_keep.grid_model_id in asset_bay.sr_switch_grid_model_id.keys() - ): - # If both busbars are present, we need to remove the one that is not kept - new_sr_switch_grid_model_id = { - key: foreign_id - for (key, foreign_id) in asset_bay.sr_switch_grid_model_id.items() - if key != busbar_to_remove.grid_model_id - } - else: - # If the target busbar is not present, we change the key - new_sr_switch_grid_model_id = { - (busbar_to_keep.grid_model_id if key == busbar_to_remove.grid_model_id else key): foreign_id - for (key, foreign_id) in asset_bay.sr_switch_grid_model_id.items() - } + Returns + ------- + Optional[AssetBay] + Updated asset bay, or `None` if no asset bay is present. + """ + if asset_bay is None: + return None + + new_sr_switch_grid_model_id = {} + for key, foreign_id in asset_bay.sr_switch_grid_model_id.items(): + if key == remove_busbar.grid_model_id and keep_busbar.grid_model_id in asset_bay.sr_switch_grid_model_id: + continue + mapped_key = keep_busbar.grid_model_id if key == remove_busbar.grid_model_id else key + new_sr_switch_grid_model_id[mapped_key] = foreign_id + + return asset_bay.model_copy(update={"sr_switch_grid_model_id": new_sr_switch_grid_model_id}) + + +def _update_asset_bays_for_fused_busbar( + asset_connections: list[MaterializedAssetConnection], + keep_busbar: Busbar, + remove_busbar: Busbar, +) -> list[MaterializedAssetConnection]: + """Apply the fused-busbar SR key rewrite to each asset connection. + + Parameters + ---------- + asset_connections : list[MaterializedAssetConnection] + Asset connections to update. + keep_busbar : Busbar + Busbar that remains after fusion. + remove_busbar : Busbar + Busbar that is removed during fusion. + + Returns + ------- + list[MaterializedAssetConnection] + Updated asset connections. + """ + return [ + asset_connection.model_copy( + update={"asset_bay": _replace_sr_keys_for_fused_busbar(asset_connection.asset_bay, keep_busbar, remove_busbar)} + ) + for asset_connection in asset_connections + ] + + +def _replace_coupler_busbar_id_for_fusion( + coupler: BusbarCoupler, + keep_busbar: Busbar, + remove_busbar: Busbar, +) -> BusbarCoupler: + """Rewrite coupler endpoints that still reference the removed busbar. + + Parameters + ---------- + coupler : BusbarCoupler + Coupler to update. + keep_busbar : Busbar + Busbar that remains after fusion. + remove_busbar : Busbar + Busbar that is removed during fusion. + + Returns + ------- + BusbarCoupler + Updated coupler with rewritten endpoints when needed. + """ + if coupler.busbar_from_id == remove_busbar.int_id: + return coupler.model_copy(update={"busbar_from_id": keep_busbar.int_id}) + if coupler.busbar_to_id == remove_busbar.int_id: + return coupler.model_copy(update={"busbar_to_id": keep_busbar.int_id}) + return coupler + + +def fuse_coupler( + station: MaterializedStation, + coupler_grid_model_id: str, + copy_info_from: bool = True, +) -> MaterializedStation: + """Fuse a coupler by merging its adjacent busbars. - return asset_bay.model_copy(update={"sr_switch_grid_model_id": new_sr_switch_grid_model_id}) - - def _replace_int_id(coupler: BusbarCoupler) -> BusbarCoupler: - """Update coupler int-ids that are pointing to the removed busbar.""" - if coupler.busbar_from_id == busbar_to_remove.int_id: - return coupler.model_copy(update={"busbar_from_id": busbar_to_keep.int_id}) - if coupler.busbar_to_id == busbar_to_remove.int_id: - return coupler.model_copy(update={"busbar_to_id": busbar_to_keep.int_id}) - return coupler - - new_busbars = [b for i, b in enumerate(station.busbars) if i != busbar_index_to_remove] - new_couplers = [_replace_int_id(c) for c in station.couplers if c.grid_model_id != coupler_grid_model_id] - new_asset_connections = [ - asset_connection.model_copy(update={"asset_bay": _replace_sr_keys(asset_connection.asset_bay)}) - for asset_connection in station.asset_connections + Parameters + ---------- + station : MaterializedStation + Station containing the coupler to fuse. The coupler open state is ignored. + coupler_grid_model_id : str + Grid model identifier of the coupler to fuse. + copy_info_from : bool, optional + If `True`, keep metadata from the busbar on the coupler's from-side. + If `False`, keep metadata from the to-side busbar. + + Returns + ------- + MaterializedStation + Station with the coupler removed and both busbars merged. + + Raises + ------ + ValueError + If the coupler is missing or if parallel couplers connect the same busbar pair. + """ + coupler = next((c for c in station.couplers if c.grid_model_id == coupler_grid_model_id), None) + if coupler is None: + raise ValueError(f"Coupler {coupler_grid_model_id} not found in station {station.grid_model_id}") + + _validate_coupler_can_be_fused(station, coupler) + busbar_from_index, busbar_to_index, keep_busbar_index, remove_busbar_index, keep_busbar, remove_busbar = ( + _resolve_fused_busbars(station, coupler, copy_info_from) + ) + + new_branch_switching_table = _merge_and_delete_fused_busbar_row( + station.branch_switching_table, + busbar_from_index, + busbar_to_index, + keep_busbar_index, + remove_busbar_index, + ) + new_injection_switching_table = _merge_and_delete_fused_busbar_row( + station.injection_switching_table, + busbar_from_index, + busbar_to_index, + keep_busbar_index, + remove_busbar_index, + ) + new_branch_connectivity = _merge_and_delete_fused_busbar_row( + station.branch_connectivity, + busbar_from_index, + busbar_to_index, + keep_busbar_index, + remove_busbar_index, + ) + new_injection_connectivity = _merge_and_delete_fused_busbar_row( + station.injection_connectivity, + busbar_from_index, + busbar_to_index, + keep_busbar_index, + remove_busbar_index, + ) + + new_busbars = [busbar for index, busbar in enumerate(station.busbars) if index != remove_busbar_index] + new_couplers = [ + _replace_coupler_busbar_id_for_fusion(other_coupler, keep_busbar, remove_busbar) + for other_coupler in station.couplers + if other_coupler.grid_model_id != coupler_grid_model_id ] + new_branch_connections = _update_asset_bays_for_fused_busbar(station.branch_connections, keep_busbar, remove_busbar) + new_injection_connections = _update_asset_bays_for_fused_busbar( + station.injection_connections, + keep_busbar, + remove_busbar, + ) station = station.model_copy( update={ "busbars": new_busbars, - "asset_switching_table": new_switching_table, - "asset_connectivity": new_connectivity_table, - "asset_connections": new_asset_connections, + "branch_connections": new_branch_connections, + "injection_connections": new_injection_connections, + "branch_switching_table": new_branch_switching_table, + "injection_switching_table": new_injection_switching_table, + "branch_connectivity": new_branch_connectivity, + "injection_connectivity": new_injection_connectivity, "couplers": new_couplers, } ) @@ -1480,31 +1872,33 @@ def fuse_all_couplers_with_type( coupler_type: str, copy_info_from: bool = True, ) -> tuple[MaterializedStation, list[BusbarCoupler]]: - """Fuses all couplers of a specific type in a station. + """Fuse all couplers of a given type in a station. - This will also filter all duplicate couplers, as there might be an edge case in which a triangle of busbars is not - properly merged otherwise. + Duplicate couplers are filtered after each fusion step so chained merges do not + leave residual parallel couplers between already fused busbars. Parameters ---------- - station : Station - The station with the couplers to fuse + station : MaterializedStation + Station containing the couplers to fuse. coupler_type : str - The type of coupler to fuse, will match the coupler.type attribute. If coupler.type is None, it will never match. + Coupler type to match against `coupler.coupler_type`. copy_info_from : bool, optional - Whether the new busbar retains the information (grid_model_id, etc) of the busbar on the from side of the coupler, - by default True. If False, the busbar on the to side of the coupler is used. + If `True`, keep metadata from the from-side busbar for each fusion. + If `False`, keep metadata from the to-side busbar. Returns ------- - Station - The station with the couplers fused that matched the type. The couplers are removed and the busbars are merged. + MaterializedStation + Station after all matching couplers have been fused. list[BusbarCoupler] - The couplers that were fused or removed due to being parallel and are no longer present in the station. + Couplers removed either by fusion or duplicate filtering. """ fused_couplers = [] while True: - coupler = next((c for c in station.couplers if (c.type is not None and c.type == coupler_type)), None) + coupler = next( + (c for c in station.couplers if (c.coupler_type is not None and c.coupler_type == coupler_type)), None + ) if coupler is None: break @@ -1525,20 +1919,20 @@ def find_station_by_id(stations: list[MaterializedStation], station_id: str) -> Parameters ---------- - stations : list[Station] - The list of stations to search in. + stations : list[MaterializedStation] + Stations to search. station_id : str - The grid_model_id of the station to find. + Grid model identifier of the station to find. Returns ------- - Station - The station with the given grid_model_id. + MaterializedStation + Matching station. Raises ------ ValueError - If the station is not found in the list. + If no station with the requested identifier is present. """ for station in stations: if station.grid_model_id == station_id: diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py b/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py index ce3c3e5eb..3f37a1aa3 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py @@ -147,27 +147,51 @@ class StationDiffArray(BaseModel): station.couplers in length and order and the entries correspond to open (True) and closed (False). The n_actions dimension provides an entry per action in the action set.""" - switching_table: Bool[np.ndarray, " _n_actions _n_busbars _n_assets"] - """The switching table of the station. The array dimensions n_busbars and n_assets are equivalent to the - switching table in the station, """ + branch_switching_table: Bool[np.ndarray, " _n_actions _n_busbars _n_branch_assets"] + """Branch switching tables for the station actions. + + The busbar and branch-asset dimensions match ``station.branch_switching_table``. + """ + + injection_switching_table: Bool[np.ndarray, " _n_actions _n_busbars _n_injection_assets"] + """Injection switching tables for the station actions. + + The busbar and injection-asset dimensions match ``station.injection_switching_table``. + """ @model_validator(mode="after") def _validate_station_diff_arrays(self) -> "StationDiffArray": """Validate stored station diff array shapes. Different stations can legitimately have different action counts, so the relevant invariant is - local to each station diff: coupler_open and switching_table must agree on their first - dimension per station. However, the beartype checker invokes the checks in such a way that a global instantiation of - dimension values was happening, raising. Hence, we check the shapes manually here. + local to each station diff: coupler_open, branch_switching_table, and injection_switching_table + must agree on their first dimension per station. The two switching tables must also agree on + their busbar dimension. However, the beartype checker invokes the checks in such a way that a + global instantiation of dimension values was happening, raising. Hence, we check the shapes + manually here. """ if self.coupler_open.ndim != 2: raise ValueError("coupler_open must be a 2D array of shape (n_actions, n_couplers)") - if self.switching_table.ndim != 3: - raise ValueError("switching_table must be a 3D array of shape (n_actions, n_busbars, n_assets)") - if self.coupler_open.shape[0] != self.switching_table.shape[0]: + if self.branch_switching_table.ndim != 3: + raise ValueError("branch_switching_table must be a 3D array of shape (n_actions, n_busbars, n_branch_assets)") + if self.injection_switching_table.ndim != 3: + raise ValueError( + "injection_switching_table must be a 3D array of shape (n_actions, n_busbars, n_injection_assets)" + ) + if self.coupler_open.shape[0] != self.branch_switching_table.shape[0]: + raise ValueError( + "coupler_open and branch_switching_table must have the same n_actions dimension, got " + f"{self.coupler_open.shape[0]} and {self.branch_switching_table.shape[0]}" + ) + if self.coupler_open.shape[0] != self.injection_switching_table.shape[0]: raise ValueError( - "coupler_open and switching_table must have the same n_actions dimension, got " - f"{self.coupler_open.shape[0]} and {self.switching_table.shape[0]}" + "coupler_open and injection_switching_table must have the same n_actions dimension, got " + f"{self.coupler_open.shape[0]} and {self.injection_switching_table.shape[0]}" + ) + if self.branch_switching_table.shape[1] != self.injection_switching_table.shape[1]: + raise ValueError( + "branch_switching_table and injection_switching_table must have the same n_busbars dimension, got " + f"{self.branch_switching_table.shape[1]} and {self.injection_switching_table.shape[1]}" ) return self @@ -221,7 +245,8 @@ def _validate_station_diff_hypothesis(starting_station: MaterializedStation, act def normalize_station(station: MaterializedStation) -> dict[str, object]: station_data = station.model_dump(mode="json") - station_data.pop("asset_switching_table", None) + station_data.pop("branch_switching_table", None) + station_data.pop("injection_switching_table", None) for coupler in station_data.get("couplers", []): if isinstance(coupler, dict): coupler.pop("open", None) @@ -229,7 +254,7 @@ def normalize_station(station: MaterializedStation) -> dict[str, object]: if normalize_station(action) != normalize_station(starting_station): raise ValueError( - f"Action station {action.grid_model_id} changed fields other than coupler open states and asset switching table." + f"Action station {action.grid_model_id} changed fields other than coupler open states and switching tables." ) @@ -260,7 +285,8 @@ def store_station_diff_fs( for station_diff in station_diffs: group = file.create_group(station_diff.grid_model_id) group.create_dataset("coupler_open", data=station_diff.coupler_open) - group.create_dataset("switching_table", data=station_diff.switching_table) + group.create_dataset("branch_switching_table", data=station_diff.branch_switching_table) + group.create_dataset("injection_switching_table", data=station_diff.injection_switching_table) bytes_io.seek(0) with filesystem.open(str(diff_file_path), "wb") as file: file.write(bytes_io.getbuffer()) @@ -294,9 +320,13 @@ def _load_station_diff_io(binaryio: io.IOBase) -> list[StationDiffArray]: for grid_model_id in station_order: group = file[grid_model_id] coupler_open = group["coupler_open"][:] - switching_table = group["switching_table"][:] + branch_switching_table = group["branch_switching_table"][:] + injection_switching_table = group["injection_switching_table"][:] station_diff = StationDiffArray( - grid_model_id=grid_model_id, coupler_open=coupler_open, switching_table=switching_table + grid_model_id=grid_model_id, + coupler_open=coupler_open, + branch_switching_table=branch_switching_table, + injection_switching_table=injection_switching_table, ) station_diffs.append(station_diff) return station_diffs @@ -336,7 +366,7 @@ def expand_single_station_diff_to_actions( ---------- starting_station : Station The station as it looks in the starting topology. All fields from the station will be copied except for the - coupler states and switching table, which will be overwritten by the station diff. + coupler states and switching tables, which will be overwritten by the station diff. station_diff : StationDiffArray The station diff to expand. @@ -352,12 +382,14 @@ def expand_single_station_diff_to_actions( coupler.model_copy(update={"open": bool(coupler_open)}) for coupler, coupler_open in zip(starting_station.couplers, coupler_array, strict=True) ] - switching_table = station_diff.switching_table[i] + branch_switching_table = station_diff.branch_switching_table[i] + injection_switching_table = station_diff.injection_switching_table[i] action = starting_station.model_copy( update={ "couplers": couplers, - "asset_switching_table": switching_table, + "branch_switching_table": branch_switching_table, + "injection_switching_table": injection_switching_table, }, ) actions.append(action) @@ -374,7 +406,7 @@ def expand_station_diffs(starting_topology: Topology, station_diffs: list[Statio starting_topology : Topology The topology as it looks in the starting topology. The station diffs will be matched to the stations in the topology based on their grid_model_id and all fields from the station will be copied except for the coupler states and - switching table, which will be overwritten by the station diff. + switching tables, which will be overwritten by the station diff. station_diffs : list[StationDiffArray] The station diffs to expand. @@ -407,7 +439,7 @@ def compress_actions_to_station_diffs( ---------- starting_topology : Topology The topology as it looks in the starting topology. The stations will be matched to the stations in the topology - based on their grid_model_id and the coupler states and switching table will be compared to the ones in the topology + based on their grid_model_id and the coupler states and switching tables will be compared to the ones in the topology to create the station diffs. Note that this should be the simplified starting topology if simplifications have been applies, as they will also be present in all stations in the action set. @@ -429,7 +461,7 @@ def compress_actions_to_station_diffs( If the actions are not grouped by station ValueError If validate_diff_hypothesis is True and the change between actions for the same station regards fields other than the - coupler states and switching table. + coupler states and switching tables. """ grid_model_id_to_station = {station.grid_model_id: station for station in starting_topology.materialize_stations()} station_diffs = {} @@ -439,22 +471,31 @@ def compress_actions_to_station_diffs( starting_station = grid_model_id_to_station[grid_model_id] coupler_open = [] - switching_table = [] + branch_switching_tables = [] + injection_switching_tables = [] for action in group: assert len(action.couplers) == len(starting_station.couplers), ( "Number of couplers in action station does not match starting station." ) - assert action.asset_switching_table.shape == starting_station.asset_switching_table.shape, ( - "Switching table shape in action station does not match starting station." + assert action.branch_switching_table.shape == starting_station.branch_switching_table.shape, ( + "Branch switching table shape in action station does not match starting station." + ) + assert action.injection_switching_table.shape == starting_station.injection_switching_table.shape, ( + "Injection switching table shape in action station does not match starting station." ) if validate_diff_hypothesis: _validate_station_diff_hypothesis(starting_station=starting_station, action=action) coupler_open.append([coupler.open for coupler in action.couplers]) - switching_table.append(action.asset_switching_table) + branch_switching_tables.append(action.branch_switching_table) + injection_switching_tables.append(action.injection_switching_table) coupler_open_array = np.array(coupler_open).astype(bool) - switching_table_array = np.array(switching_table).astype(bool) + branch_switching_table_array = np.array(branch_switching_tables).astype(bool) + injection_switching_table_array = np.array(injection_switching_tables).astype(bool) station_diff = StationDiffArray( - grid_model_id=grid_model_id, coupler_open=coupler_open_array, switching_table=switching_table_array + grid_model_id=grid_model_id, + coupler_open=coupler_open_array, + branch_switching_table=branch_switching_table_array, + injection_switching_table=injection_switching_table_array, ) if station_diff.grid_model_id in station_diffs: raise ValueError(f"Duplicate station diff for grid_model_id {grid_model_id}, actions were not in order.") diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index c989b16fb..c47ab6bdd 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -14,8 +14,10 @@ from pydantic import ValidationError from toop_engine_interfaces.asset_topology import ( AssetBay, + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, MaterializedAssetConnection, MaterializedStation, RawStation, @@ -23,6 +25,7 @@ SwitchableAsset, Topology, build_asset_bay_id, + normalize_switchable_asset_payload, topology_from_materialized_stations, ) from toop_engine_interfaces.asset_topology_helpers import ( @@ -67,13 +70,91 @@ def raw_asset_connections( ] +def make_materialized_station( + *, + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + branch_assets: list[SwitchableAsset], + branch_switching_table: np.ndarray, + branch_connectivity: np.ndarray | None = None, + branch_terminals: list[str | None] | None = None, + branch_asset_bays: list[AssetBay | None] | None = None, + injection_assets: list[SwitchableAsset] | None = None, + injection_switching_table: np.ndarray | None = None, + injection_connectivity: np.ndarray | None = None, + injection_terminals: list[str | None] | None = None, + injection_asset_bays: list[AssetBay | None] | None = None, +) -> MaterializedStation: + n_busbars = len(busbars) + resolved_injection_assets = injection_assets or [] + resolved_injection_switching = injection_switching_table + if resolved_injection_switching is None: + resolved_injection_switching = np.zeros((n_busbars, len(resolved_injection_assets)), dtype=bool) + + return MaterializedStation( + grid_model_id=grid_model_id, + busbars=busbars, + couplers=couplers, + branch_connections=materialized_asset_connections(branch_assets, branch_terminals, branch_asset_bays), + injection_connections=materialized_asset_connections( + resolved_injection_assets, + injection_terminals, + injection_asset_bays, + ), + branch_switching_table=branch_switching_table, + injection_switching_table=resolved_injection_switching, + branch_connectivity=branch_connectivity, + injection_connectivity=injection_connectivity, + ) + + +def make_raw_station( + *, + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + branch_asset_ids: list[str], + branch_switching_table: np.ndarray, + branch_connectivity: np.ndarray | None = None, + branch_terminals: list[str | None] | None = None, + branch_asset_bay_ids: list[str | None] | None = None, + injection_asset_ids: list[str] | None = None, + injection_switching_table: np.ndarray | None = None, + injection_connectivity: np.ndarray | None = None, + injection_terminals: list[str | None] | None = None, + injection_asset_bay_ids: list[str | None] | None = None, +) -> RawStation: + n_busbars = len(busbars) + resolved_injection_asset_ids = injection_asset_ids or [] + resolved_injection_switching = injection_switching_table + if resolved_injection_switching is None: + resolved_injection_switching = np.zeros((n_busbars, len(resolved_injection_asset_ids)), dtype=bool) + + return RawStation( + grid_model_id=grid_model_id, + busbars=busbars, + couplers=couplers, + branch_connections=raw_asset_connections(branch_asset_ids, branch_terminals, branch_asset_bay_ids), + injection_connections=raw_asset_connections( + resolved_injection_asset_ids, + injection_terminals, + injection_asset_bay_ids, + ), + branch_switching_table=branch_switching_table, + injection_switching_table=resolved_injection_switching, + branch_connectivity=branch_connectivity, + injection_connectivity=injection_connectivity, + ) + + def test_station() -> None: assets = [ SwitchableAsset(grid_model_id="line1"), SwitchableAsset(grid_model_id="line2"), SwitchableAsset(grid_model_id="line3"), ] - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -81,15 +162,15 @@ def test_station() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) assert station is not None with pytest.raises(ValidationError): # Wrong shape of switching table - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -102,14 +183,14 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]).T, + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]).T, grid_model_id="station1", ) with pytest.raises(ValidationError): # Coupler references non-existing busbar - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -122,14 +203,14 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) with pytest.raises(ValidationError): # Coupler references non-existing busbar - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -142,14 +223,14 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) with pytest.raises(ValidationError): # Busbar int_id is not unique - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=1, grid_model_id="busbar2"), @@ -162,13 +243,13 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) with pytest.raises(ValidationError): - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -188,15 +269,13 @@ def test_station() -> None: grid_model_id="coupler2", ), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", in_service=False), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4", in_service=False), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2", in_service=False), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4", in_service=False), + ], + branch_switching_table=np.array( [ [True, False, True, True], [False, True, False, False], @@ -207,7 +286,7 @@ def test_station() -> None: ) with pytest.raises(ValidationError): - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -220,12 +299,12 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), @@ -233,8 +312,8 @@ def test_station() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="bus_id1", ) assert station is not None @@ -261,7 +340,7 @@ def test_station() -> None: assert not station_with_empty_bus_id.is_split() with pytest.raises(ValidationError): - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), @@ -274,8 +353,8 @@ def test_station() -> None: grid_model_id="coupler1", ), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="bus_id0", ) @@ -296,12 +375,12 @@ def test_station_connectivity_tables(): asset_switching_table = np.array([[True, False, True], [False, True, False]]) asset_connectivity = np.array([[True, True, True], [True, True, True]]) grid_model_id = "station1" - station = MaterializedStation( + station = make_materialized_station( busbars=busbars, couplers=couplers, - asset_connections=materialized_asset_connections(assets), - asset_switching_table=asset_switching_table, - asset_connectivity=asset_connectivity, + branch_assets=assets, + branch_switching_table=asset_switching_table, + branch_connectivity=asset_connectivity, grid_model_id=grid_model_id, ) assert station is not None @@ -310,38 +389,38 @@ def test_station_connectivity_tables(): # entry in asset_switching_table is not in asset_connectivity asset_switching_table = np.array([[True, False, True], [False, True, False]]) asset_connectivity = np.array([[True, True, True], [True, False, True]]) - station = MaterializedStation( + station = make_materialized_station( busbars=busbars, couplers=couplers, - asset_connections=materialized_asset_connections(assets), - asset_switching_table=asset_switching_table, - asset_connectivity=asset_connectivity, + branch_assets=assets, + branch_switching_table=asset_switching_table, + branch_connectivity=asset_connectivity, grid_model_id=grid_model_id, ) asset_switching_table = np.array([[True, False, True], [False, True, False]]) asset_connectivity = np.array([[True, True, True], [True, True, True]]) - station = MaterializedStation( + station = make_materialized_station( busbars=busbars, couplers=couplers, - asset_connections=materialized_asset_connections(assets), - asset_switching_table=asset_switching_table, - asset_connectivity=asset_connectivity, + branch_assets=assets, + branch_switching_table=asset_switching_table, + branch_connectivity=asset_connectivity, grid_model_id=grid_model_id, ) assert station is not None def test_topology_station_is_split() -> None: - station = RawStation( + station = make_raw_station( grid_model_id="bus_id1", busbars=[ Busbar(int_id=1, grid_model_id="busbar1", bus_branch_bus_id="bus_id1"), Busbar(int_id=2, grid_model_id="busbar2", bus_branch_bus_id="bus_id1"), ], couplers=[], - asset_connections=raw_asset_connections(["line1"]), - asset_switching_table=np.array([[True], [False]]), + branch_asset_ids=["line1"], + branch_switching_table=np.array([[True], [False]]), ) assert not station.is_split() @@ -372,8 +451,10 @@ def test_schema() -> None: assert schema is not None assert "busbars" in schema["properties"] assert "couplers" in schema["properties"] - assert "asset_connections" in schema["properties"] - assert "asset_switching_table" in schema["properties"] + assert "branch_connections" in schema["properties"] + assert "branch_switching_table" in schema["properties"] + assert "injection_connections" in schema["properties"] + assert "injection_switching_table" in schema["properties"] assert "grid_model_id" in schema["properties"] @@ -383,7 +464,7 @@ def test_serialize_station() -> None: SwitchableAsset(grid_model_id="line2"), SwitchableAsset(grid_model_id="line3"), ] - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -391,8 +472,8 @@ def test_serialize_station() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -408,7 +489,7 @@ def test_load_asset_topology() -> None: SwitchableAsset(grid_model_id="line2"), SwitchableAsset(grid_model_id="line3"), ] - station1 = MaterializedStation( + station1 = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -416,8 +497,8 @@ def test_load_asset_topology() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections(station1_assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=station1_assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station1", ) @@ -426,7 +507,7 @@ def test_load_asset_topology() -> None: SwitchableAsset(grid_model_id="line5"), SwitchableAsset(grid_model_id="line6"), ] - station2 = MaterializedStation( + station2 = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar3"), Busbar(int_id=2, grid_model_id="busbar4"), @@ -434,8 +515,8 @@ def test_load_asset_topology() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler2"), ], - asset_connections=materialized_asset_connections(station2_assets), - asset_switching_table=np.array([[True, False, True], [False, True, False]]), + branch_assets=station2_assets, + branch_switching_table=np.array([[True, False, True], [False, True, False]]), grid_model_id="station2", ) @@ -468,7 +549,7 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: ), None, ] - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -476,8 +557,14 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections(assets, ["from", None], asset_bays), - asset_switching_table=np.array([[True, False], [False, True]]), + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + branch_terminals=["from"], + branch_asset_bays=[asset_bays[0]], + branch_switching_table=np.array([[True], [False]]), + injection_assets=[InjectionAsset(grid_model_id="load1", asset_type="load")], + injection_terminals=[None], + injection_asset_bays=[asset_bays[1]], + injection_switching_table=np.array([[False], [True]]), grid_model_id="station1", ) @@ -495,11 +582,14 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: grid_model_id="station1", busbars=station.busbars, couplers=station.couplers, - asset_connections=raw_asset_connections(["line1", "load1"], ["from", None], ["station1::line1::bay", None]), - asset_switching_table=np.array([[True, False], [False, True]]), + branch_connections=raw_asset_connections(["line1"], ["from"], ["station1::line1::bay"]), + injection_connections=raw_asset_connections(["load1"], [None], [None]), + branch_switching_table=np.array([[True], [False]]), + injection_switching_table=np.array([[False], [True]]), ) ] - assert [asset.grid_model_id for asset in topology.assets] == ["line1", "load1"] + assert [asset.grid_model_id for asset in topology.branch_assets] == ["line1"] + assert [asset.grid_model_id for asset in topology.injection_assets] == ["load1"] assert [asset_bay.asset_bay_id for asset_bay in topology.asset_bays] == ["station1::line1::bay"] materialized_station = topology.materialize_stations()[0] @@ -507,22 +597,24 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_two_station_views() -> None: - asset_from = SwitchableAsset(grid_model_id="line1", type="line") - asset_to = SwitchableAsset(grid_model_id="line1", type="line") + asset_from = BranchAsset(grid_model_id="line1", asset_type="line") + asset_to = BranchAsset(grid_model_id="line1", asset_type="line") - station_from = MaterializedStation( + station_from = make_materialized_station( grid_model_id="station_from", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], - asset_connections=materialized_asset_connections([asset_from], ["from"]), - asset_switching_table=np.array([[True]]), + branch_assets=[asset_from], + branch_terminals=["from"], + branch_switching_table=np.array([[True]]), ) - station_to = MaterializedStation( + station_to = make_materialized_station( grid_model_id="station_to", busbars=[Busbar(int_id=1, grid_model_id="busbar2")], couplers=[], - asset_connections=materialized_asset_connections([asset_to], ["to"]), - asset_switching_table=np.array([[True]]), + branch_assets=[asset_to], + branch_terminals=["to"], + branch_switching_table=np.array([[True]]), ) topology = topology_from_materialized_stations( @@ -534,48 +626,105 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw stations=[station_from, station_to], ) - assert [asset.grid_model_id for asset in topology.assets] == ["line1"] - assert [connection.asset_id for connection in topology.raw_stations[0].asset_connections] == ["line1"] - assert [connection.terminal for connection in topology.raw_stations[0].asset_connections] == ["from"] - assert [connection.asset_id for connection in topology.raw_stations[1].asset_connections] == ["line1"] - assert [connection.terminal for connection in topology.raw_stations[1].asset_connections] == ["to"] + assert [asset.grid_model_id for asset in topology.branch_assets] == ["line1"] + assert [connection.asset_id for connection in topology.raw_stations[0].branch_connections] == ["line1"] + assert [connection.terminal for connection in topology.raw_stations[0].branch_connections] == ["from"] + assert [connection.asset_id for connection in topology.raw_stations[1].branch_connections] == ["line1"] + assert [connection.terminal for connection in topology.raw_stations[1].branch_connections] == ["to"] assert topology.materialize_stations() == [station_from, station_to] -def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_station() -> None: - station_from = MaterializedStation( +def test_topology_from_materialized_stations_normalizes_equivalent_branch_asset_payloads() -> None: + station_from = make_materialized_station( grid_model_id="station_from", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], - asset_connections=materialized_asset_connections( - [SwitchableAsset(grid_model_id="line1", type="line")], - ["from"], - [ - AssetBay( - asset_bay_id=build_asset_bay_id("station_from", "line1"), - dv_switch_grid_model_id="dv_from", - sr_switch_grid_model_id={"busbar1": "sr_from"}, - ) - ], - ), - asset_switching_table=np.array([[True]]), + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + branch_terminals=["from"], + branch_switching_table=np.array([[True]]), ) - station_to = MaterializedStation( + station_to = make_materialized_station( grid_model_id="station_to", busbars=[Busbar(int_id=1, grid_model_id="busbar2")], couplers=[], - asset_connections=materialized_asset_connections( - [SwitchableAsset(grid_model_id="line1", type="line")], - ["to"], - [ - AssetBay( - asset_bay_id=build_asset_bay_id("station_to", "line1"), - dv_switch_grid_model_id="dv_to", - sr_switch_grid_model_id={"busbar2": "sr_to"}, - ) - ], + branch_assets=[SwitchableAsset(grid_model_id="line1", asset_type="line")], + branch_terminals=["to"], + branch_switching_table=np.array([[True]]), + ) + + topology = topology_from_materialized_stations( + reference_topology=Topology( + topology_id="topology1", + raw_stations=[], + timestamp=datetime.now(), ), - asset_switching_table=np.array([[True]]), + stations=[station_from, station_to], + ) + + assert len(topology.branch_assets) == 1 + assert isinstance(topology.branch_assets[0], BranchAsset) + assert topology.branch_assets[0].grid_model_id == "line1" + + +def test_raw_station_model_copy_revalidates_updates() -> None: + station = make_raw_station( + grid_model_id="station1", + busbars=[Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2")], + couplers=[], + branch_asset_ids=["line1"], + branch_switching_table=np.array([[True], [False]]), + ) + + with pytest.raises(ValidationError, match="branch_switching_table shape"): + station.model_copy(update={"branch_switching_table": np.array([[True]], dtype=bool)}) + + +def test_raw_station_model_copy_honors_deep_flag_for_nested_models() -> None: + station = make_raw_station( + grid_model_id="station1", + busbars=[Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2")], + couplers=[], + branch_asset_ids=["line1"], + branch_switching_table=np.array([[True], [False]]), + ) + + shallow_copy = station.model_copy() + deep_copy = station.model_copy(deep=True) + + assert shallow_copy.busbars[0] is station.busbars[0] + assert deep_copy.busbars[0] is not station.busbars[0] + + +def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_station() -> None: + station_from = make_materialized_station( + grid_model_id="station_from", + busbars=[Busbar(int_id=1, grid_model_id="busbar1")], + couplers=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + branch_terminals=["from"], + branch_asset_bays=[ + AssetBay( + asset_bay_id=build_asset_bay_id("station_from", "line1"), + dv_switch_grid_model_id="dv_from", + sr_switch_grid_model_id={"busbar1": "sr_from"}, + ) + ], + branch_switching_table=np.array([[True]]), + ) + station_to = make_materialized_station( + grid_model_id="station_to", + busbars=[Busbar(int_id=1, grid_model_id="busbar2")], + couplers=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + branch_terminals=["to"], + branch_asset_bays=[ + AssetBay( + asset_bay_id=build_asset_bay_id("station_to", "line1"), + dv_switch_grid_model_id="dv_to", + sr_switch_grid_model_id={"busbar2": "sr_to"}, + ) + ], + branch_switching_table=np.array([[True]]), ) topology = topology_from_materialized_stations( @@ -591,39 +740,36 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ "station_from::line1::bay", "station_to::line1::bay", ] - assert [connection.asset_bay_id for connection in topology.raw_stations[0].asset_connections] == [ + assert [connection.asset_bay_id for connection in topology.raw_stations[0].branch_connections] == [ "station_from::line1::bay" ] - assert [connection.asset_bay_id for connection in topology.raw_stations[1].asset_connections] == [ + assert [connection.asset_bay_id for connection in topology.raw_stations[1].branch_connections] == [ "station_to::line1::bay" ] def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_occurrence() -> None: - station = MaterializedStation( + station = make_materialized_station( grid_model_id="station1", busbars=[Busbar(int_id=1, grid_model_id="busbar1")], couplers=[], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1", type="line"), - SwitchableAsset(grid_model_id="line1", type="line"), - ], - None, - [ - AssetBay( - asset_bay_id=build_asset_bay_id("station1", "line1"), - dv_switch_grid_model_id="dv1", - sr_switch_grid_model_id={"busbar1": "sr1"}, - ), - AssetBay( - asset_bay_id=build_asset_bay_id("station1", "line1", 1), - dv_switch_grid_model_id="dv2", - sr_switch_grid_model_id={"busbar1": "sr2"}, - ), - ], - ), - asset_switching_table=np.array([[True, True]]), + branch_assets=[ + BranchAsset(grid_model_id="line1", asset_type="line"), + BranchAsset(grid_model_id="line1", asset_type="line"), + ], + branch_asset_bays=[ + AssetBay( + asset_bay_id=build_asset_bay_id("station1", "line1"), + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ), + AssetBay( + asset_bay_id=build_asset_bay_id("station1", "line1", 1), + dv_switch_grid_model_id="dv2", + sr_switch_grid_model_id={"busbar1": "sr2"}, + ), + ], + branch_switching_table=np.array([[True, True]]), ) topology = topology_from_materialized_stations( @@ -635,8 +781,8 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ stations=[station], ) - assert [connection.asset_id for connection in topology.raw_stations[0].asset_connections] == ["line1", "line1"] - assert [connection.asset_bay_id for connection in topology.raw_stations[0].asset_connections] == [ + assert [connection.asset_id for connection in topology.raw_stations[0].branch_connections] == ["line1", "line1"] + assert [connection.asset_bay_id for connection in topology.raw_stations[0].branch_connections] == [ "station1::line1::bay", "station1::line1::bay::1", ] @@ -653,7 +799,7 @@ def test_filter_out_of_service() -> None: SwitchableAsset(grid_model_id="line3"), SwitchableAsset(grid_model_id="line4", in_service=False), ] - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -663,8 +809,8 @@ def test_filter_out_of_service() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler2"), ], - asset_connections=materialized_asset_connections(assets), - asset_switching_table=np.array( + branch_assets=assets, + branch_switching_table=np.array( [ [True, False, True, True], [False, True, False, False], @@ -677,12 +823,12 @@ def test_filter_out_of_service() -> None: station = filter_out_of_service(station) assert len(station.busbars) == 2 assert len(station.couplers) == 1 - assert len(station.asset_connections) == 2 - assert np.array_equal(station.asset_switching_table, np.array([[True, True], [False, False]])) + assert len(station.branch_connections) == 2 + assert np.array_equal(station.branch_switching_table, np.array([[True, True], [False, False]])) def test_has_transmission_line_switching() -> None: - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -692,15 +838,13 @@ def test_has_transmission_line_switching() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler2"), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2", in_service=False), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4", in_service=False), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2", in_service=False), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4", in_service=False), + ], + branch_switching_table=np.array( [ [True, False, True, False], [False, True, False, False], @@ -712,7 +856,7 @@ def test_has_transmission_line_switching() -> None: assert has_transmission_line_switching(station) is False - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -722,15 +866,13 @@ def test_has_transmission_line_switching() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler2"), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ], + branch_switching_table=np.array( [ [True, False, True, True], [False, False, False, False], @@ -744,7 +886,7 @@ def test_has_transmission_line_switching() -> None: def test_filter_duplicate_couplers() -> None: - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -756,15 +898,13 @@ def test_filter_duplicate_couplers() -> None: BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler3"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=True, grid_model_id="coupler4"), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ], + branch_switching_table=np.array( [ [True, False, True, True], [False, True, False, False], @@ -789,7 +929,7 @@ def test_filter_duplicate_couplers() -> None: def test_filter_disconnected_busbars() -> None: - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -798,15 +938,13 @@ def test_filter_disconnected_busbars() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ], + branch_switching_table=np.array( [ [True, False, True, True], [False, True, False, False], @@ -819,9 +957,9 @@ def test_filter_disconnected_busbars() -> None: station, removed = filter_disconnected_busbars(station) assert len(station.busbars) == 2 assert len(station.couplers) == 1 - assert len(station.asset_connections) == 4 + assert len(station.branch_connections) == 4 assert np.array_equal( - station.asset_switching_table, + station.branch_switching_table, np.array([[True, False, True, True], [False, True, False, False]]), ) assert len(removed) == 1 @@ -829,7 +967,7 @@ def test_filter_disconnected_busbars() -> None: def test_filter_disconnected_busbars_sort_by_asset_count() -> None: - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -838,15 +976,13 @@ def test_filter_disconnected_busbars_sort_by_asset_count() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ], + branch_switching_table=np.array( [ [False, False, False, False], [False, True, False, False], @@ -860,9 +996,9 @@ def test_filter_disconnected_busbars_sort_by_asset_count() -> None: assert len(station.busbars) == 1 assert station.busbars[0].grid_model_id == "busbar3" assert len(station.couplers) == 0 - assert len(station.asset_connections) == 4 + assert len(station.branch_connections) == 4 assert np.array_equal( - station.asset_switching_table, + station.branch_switching_table, np.array([[True, False, True, True]]), ) assert len(removed) == 2 @@ -871,7 +1007,7 @@ def test_filter_disconnected_busbars_sort_by_asset_count() -> None: def test_select_one_for_multi_connected_assets() -> None: - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -881,15 +1017,13 @@ def test_select_one_for_multi_connected_assets() -> None: BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=False, grid_model_id="coupler2"), ], - asset_connections=materialized_asset_connections( - [ - SwitchableAsset(grid_model_id="line1"), - SwitchableAsset(grid_model_id="line2"), - SwitchableAsset(grid_model_id="line3"), - SwitchableAsset(grid_model_id="line4"), - ] - ), - asset_switching_table=np.array( + branch_assets=[ + SwitchableAsset(grid_model_id="line1"), + SwitchableAsset(grid_model_id="line2"), + SwitchableAsset(grid_model_id="line3"), + SwitchableAsset(grid_model_id="line4"), + ], + branch_switching_table=np.array( [ [True, False, True, False], [False, True, False, True], @@ -900,9 +1034,9 @@ def test_select_one_for_multi_connected_assets() -> None: ) station, removed = fix_multi_connected_without_coupler(station) - assert station.asset_switching_table[:, 0].sum() == 1 + assert station.branch_switching_table[:, 0].sum() == 1 assert np.array_equal( - station.asset_switching_table, + station.branch_switching_table, np.array( [ [False, False, True, False], @@ -915,7 +1049,7 @@ def test_select_one_for_multi_connected_assets() -> None: def test_filter_assets_by_type() -> None: - station = MaterializedStation( + station = make_materialized_station( busbars=[ Busbar(int_id=1, grid_model_id="busbar1"), Busbar(int_id=2, grid_model_id="busbar2"), @@ -923,38 +1057,47 @@ def test_filter_assets_by_type() -> None: couplers=[ BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1"), ], - asset_connections=materialized_asset_connections( + branch_assets=[ + BranchAsset(grid_model_id="line1", asset_type="line"), + BranchAsset(grid_model_id="line2", asset_type="line"), + SwitchableAsset(grid_model_id="line3", asset_type=None), + ], + branch_switching_table=np.array( [ - SwitchableAsset(grid_model_id="line1", type="line"), - SwitchableAsset(grid_model_id="line2", type="line"), - SwitchableAsset(grid_model_id="gen1", type="gen"), - SwitchableAsset(grid_model_id="load1", type="load"), - SwitchableAsset(grid_model_id="line3", type=None), + [True, False, True], + [False, True, False], ] ), - asset_switching_table=np.array( + injection_assets=[ + InjectionAsset(grid_model_id="gen1", asset_type="gen"), + InjectionAsset(grid_model_id="load1", asset_type="load"), + ], + injection_switching_table=np.array( [ - [True, False, True, False, True], - [False, True, False, True, False], + [True, False], + [False, True], ] ), grid_model_id="station1", ) station_filtered, removed = filter_assets_by_type(station, set(["line", "trafo"])) - assert len(station_filtered.asset_connections) == 2 + assert len(station_filtered.branch_connections) == 2 + assert len(station_filtered.injection_connections) == 0 assert len(removed) == 3 - assert station_filtered.asset_connections[0].asset.grid_model_id == "line1" - assert station_filtered.asset_connections[1].asset.grid_model_id == "line2" - assert station_filtered.asset_switching_table.shape == (2, 2) + assert station_filtered.branch_connections[0].asset.grid_model_id == "line1" + assert station_filtered.branch_connections[1].asset.grid_model_id == "line2" + assert station_filtered.branch_switching_table.shape == (2, 2) station_filtered, removed = filter_assets_by_type(station, set(["line", "gen"]), allow_none_type=True) - assert len(station_filtered.asset_connections) == 4 + assert len(station_filtered.branch_connections) == 3 + assert len(station_filtered.injection_connections) == 1 assert len(removed) == 1 - assert station_filtered.asset_connections[0].asset.grid_model_id == "line1" - assert station_filtered.asset_connections[1].asset.grid_model_id == "line2" - assert station_filtered.asset_connections[2].asset.grid_model_id == "gen1" - assert station_filtered.asset_connections[3].asset.grid_model_id == "line3" + combined_connections = [*station_filtered.branch_connections, *station_filtered.injection_connections] + assert combined_connections[0].asset.grid_model_id == "line1" + assert combined_connections[1].asset.grid_model_id == "line2" + assert combined_connections[2].asset.grid_model_id == "line3" + assert combined_connections[3].asset.grid_model_id == "gen1" def test_asset_bay() -> None: @@ -1027,11 +1170,12 @@ def test_station_bay() -> None: grid_model_id = "station1" # test valid Station - station = MaterializedStation( + station = make_materialized_station( busbars=busbars, couplers=couplers, - asset_connections=materialized_asset_connections(assets, asset_bays=[None, path, None]), - asset_switching_table=asset_switching_table, + branch_assets=assets, + branch_asset_bays=[None, path, None], + branch_switching_table=asset_switching_table, grid_model_id=grid_model_id, ) assert station is not None @@ -1049,21 +1193,25 @@ def test_station_bay() -> None: SwitchableAsset(grid_model_id="line3"), ] with pytest.raises(ValidationError, match="busbar_id busbar3 in asset line2 does not exist in busbars"): - station = MaterializedStation( + station = make_materialized_station( busbars=busbars, couplers=couplers, - asset_connections=materialized_asset_connections(assets, asset_bays=[None, path_error, None]), - asset_switching_table=asset_switching_table, + branch_assets=assets, + branch_asset_bays=[None, path_error, None], + branch_switching_table=asset_switching_table, grid_model_id=grid_model_id, ) def test_disambiguate_type() -> None: - asset = SwitchableAsset(grid_model_id="line", type=None) + asset = normalize_switchable_asset_payload({"grid_model_id": "line", "asset_type": None}) assert asset.is_branch() is None + assert type(asset) is SwitchableAsset - asset = SwitchableAsset(grid_model_id="line", type="line") + asset = normalize_switchable_asset_payload({"grid_model_id": "line", "asset_type": "line"}) assert asset.is_branch() is True + assert isinstance(asset, BranchAsset) - asset = SwitchableAsset(grid_model_id="gen", type="gen") + asset = normalize_switchable_asset_payload({"grid_model_id": "gen", "asset_type": "gen"}) assert asset.is_branch() is False + assert isinstance(asset, InjectionAsset) diff --git a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py index c4de76076..2f591eb30 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py +++ b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py @@ -10,8 +10,10 @@ import numpy as np import pytest from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, MaterializedAssetConnection, MaterializedStation, RawStation, @@ -26,7 +28,6 @@ find_busbars_for_coupler, fuse_all_couplers_with_type, fuse_coupler, - get_connected_assets, has_transmission_line_switching, merge_stations, order_station_assets, @@ -43,6 +44,8 @@ def build_raw_station( couplers: list[BusbarCoupler], asset_ids: list[str], asset_switching_table: np.ndarray, + injection_asset_ids: list[str] | None = None, + injection_switching_table: np.ndarray | None = None, asset_terminals: list[str | None] | None = None, asset_bay_ids: list[str | None] | None = None, ) -> RawStation: @@ -70,15 +73,22 @@ def build_raw_station( RawStation Raw station representation suitable for topology construction in tests. """ + resolved_injection_asset_ids = injection_asset_ids if injection_asset_ids is not None else [] + resolved_injection_switching_table = ( + injection_switching_table + if injection_switching_table is not None + else np.zeros((len(busbars), len(resolved_injection_asset_ids)), dtype=bool) + ) + return RawStation( grid_model_id=grid_model_id, name=None, - type=None, + station_type=None, region=None, voltage_level=None, busbars=busbars, couplers=couplers, - asset_connections=[ + branch_connections=[ StationAssetConnection(asset_id=asset_id, terminal=asset_terminal, asset_bay_id=asset_bay_id) for asset_id, asset_terminal, asset_bay_id in zip( asset_ids, @@ -87,8 +97,14 @@ def build_raw_station( strict=True, ) ], - asset_switching_table=asset_switching_table, - asset_connectivity=None, + injection_connections=[ + StationAssetConnection(asset_id=asset_id, terminal=None, asset_bay_id=None) + for asset_id in resolved_injection_asset_ids + ], + branch_switching_table=asset_switching_table, + injection_switching_table=resolved_injection_switching_table, + branch_connectivity=None, + injection_connectivity=None, model_log=None, ) @@ -100,15 +116,40 @@ def build_materialized_station( assets: list[SwitchableAsset], asset_switching_table: np.ndarray, asset_connectivity: np.ndarray | None = None, + injection_assets: list[SwitchableAsset] | None = None, + injection_switching_table: np.ndarray | None = None, + injection_connectivity: np.ndarray | None = None, ) -> MaterializedStation: """Build a materialized station using explicit asset connections.""" + resolved_injection_assets = injection_assets if injection_assets is not None else [] + resolved_injection_switching_table = ( + injection_switching_table + if injection_switching_table is not None + else np.zeros((len(busbars), len(resolved_injection_assets)), dtype=bool) + ) + resolved_injection_connectivity = injection_connectivity if injection_connectivity is not None else None + branch_assets = [ + asset if isinstance(asset, BranchAsset) else BranchAsset.model_validate(asset.model_dump()) for asset in assets + ] + normalized_injection_assets = [ + asset if isinstance(asset, InjectionAsset) else InjectionAsset.model_validate(asset.model_dump()) + for asset in resolved_injection_assets + ] return MaterializedStation( grid_model_id=grid_model_id, + name=None, + station_type=None, + region=None, + voltage_level=None, busbars=busbars, couplers=couplers, - asset_connections=[MaterializedAssetConnection(asset=asset) for asset in assets], - asset_switching_table=asset_switching_table, - asset_connectivity=asset_connectivity, + branch_connections=[MaterializedAssetConnection(asset=asset) for asset in branch_assets], + injection_connections=[MaterializedAssetConnection(asset=asset) for asset in normalized_injection_assets], + branch_switching_table=asset_switching_table, + injection_switching_table=resolved_injection_switching_table, + branch_connectivity=asset_connectivity, + injection_connectivity=resolved_injection_connectivity, + model_log=None, ) @@ -129,11 +170,27 @@ def build_test_topology(topology_id: str, raw_stations: list[RawStation], assets Topology Topology instance for helper-focused tests. """ + branch_assets = [ + asset if isinstance(asset, BranchAsset) else BranchAsset.model_validate(asset.model_dump()) + for asset in assets + if not isinstance(asset, InjectionAsset) + ] + injection_assets = [ + asset if isinstance(asset, InjectionAsset) else InjectionAsset.model_validate(asset.model_dump()) + for asset in assets + if isinstance(asset, InjectionAsset) + ] + return Topology( topology_id=topology_id, + grid_model_file=None, + name=None, raw_stations=raw_stations, - assets=assets, + branch_assets=branch_assets, + injection_assets=injection_assets, + asset_setpoints=None, timestamp=datetime.now(), + metrics=None, ) @@ -185,7 +242,7 @@ def test_merge_stations(): ] } ), - station2.model_copy(update={"asset_switching_table": np.array([[False, True, True], [True, False, False]])}), + station2.model_copy(update={"branch_switching_table": np.array([[False, True, True], [True, False, False]])}), ] updated_stations, coupler_diff, reassignment_diff = merge_stations(original_stations, new_stations, "raise") @@ -197,7 +254,7 @@ def test_merge_stations(): assert updated_stations[1].grid_model_id == "station2" assert np.array_equal( - updated_stations[1].asset_switching_table, + updated_stations[1].branch_switching_table, np.array([[False, True, True], [True, False, False]]), ) assert set(reassignment_diff) == set([("station2", 1, 0, True), ("station2", 1, 1, False)]) @@ -251,7 +308,7 @@ def test_merge_stations_append_behavior(): ] } ), - station2.model_copy(update={"asset_switching_table": np.array([[False, True, True], [True, False, False]])}), + station2.model_copy(update={"branch_switching_table": np.array([[False, True, True], [True, False, False]])}), ] updated_stations, coupler_diff, reassignment_diff = merge_stations(original_stations, new_stations, "append") @@ -263,7 +320,7 @@ def test_merge_stations_append_behavior(): assert updated_stations[1].grid_model_id == "station2" assert np.array_equal( - updated_stations[1].asset_switching_table, + updated_stations[1].branch_switching_table, np.array([[False, True, True], [True, False, False]]), ) assert reassignment_diff == [] @@ -375,22 +432,46 @@ def test_get_connected_assets(): ) # Test for busbar 0 - connected_assets_busbar_0 = get_connected_assets(station, 0) + connected_assets_busbar_0 = station.get_connected_assets(0) assert len(connected_assets_busbar_0) == 2 assert connected_assets_busbar_0[0].grid_model_id == "line1" assert connected_assets_busbar_0[1].grid_model_id == "line3" # Test for busbar 1 - connected_assets_busbar_1 = get_connected_assets(station, 1) + connected_assets_busbar_1 = station.get_connected_assets(1) assert len(connected_assets_busbar_1) == 0 # Test with no assets in service - station.asset_connections[0].asset.in_service = False - station.asset_connections[2].asset.in_service = False - connected_assets_busbar_0 = get_connected_assets(station, 0) + station.branch_connections[0].asset.in_service = False + station.branch_connections[2].asset.in_service = False + connected_assets_busbar_0 = station.get_connected_assets(0) assert len(connected_assets_busbar_0) == 0 +def test_get_connected_assets_raw_station_injection_scope(): + station = RawStation( + grid_model_id="station1", + busbars=[ + Busbar(int_id=1, grid_model_id="busbar1"), + Busbar(int_id=2, grid_model_id="busbar2"), + ], + couplers=[], + branch_connections=[StationAssetConnection(asset_id="line1", terminal="from", asset_bay_id=None)], + injection_connections=[StationAssetConnection(asset_id="gen1", terminal=None, asset_bay_id=None)], + branch_switching_table=np.array([[True], [False]]), + injection_switching_table=np.array([[False], [True]]), + ) + + connected_assets_busbar_1 = station.get_connected_assets( + 1, + topology_assets=[InjectionAsset(grid_model_id="gen1", in_service=True)], + asset_scope="injection", + ) + + assert len(connected_assets_busbar_1) == 1 + assert connected_assets_busbar_1[0].grid_model_id == "gen1" + + def test_find_busbars_for_coupler(): busbars = [ Busbar(int_id=1, grid_model_id="busbar1"), @@ -441,8 +522,10 @@ def test_station_diff_no_changes(): assert realized_station.station == station assert realized_station.coupler_diff == [] - assert realized_station.reassignment_diff == [] - assert realized_station.disconnection_diff == [] + assert realized_station.branch_reassignment_diff == [] + assert realized_station.injection_reassignment_diff == [] + assert realized_station.branch_disconnection_diff == [] + assert realized_station.injection_disconnection_diff == [] def test_station_diff_coupler_state_change(): @@ -473,8 +556,10 @@ def test_station_diff_coupler_state_change(): assert realized_station.station == target_station assert realized_station.coupler_diff == [target_station.couplers[0]] - assert realized_station.reassignment_diff == [] - assert realized_station.disconnection_diff == [] + assert realized_station.branch_reassignment_diff == [] + assert realized_station.injection_reassignment_diff == [] + assert realized_station.branch_disconnection_diff == [] + assert realized_station.injection_disconnection_diff == [] def test_station_diff_asset_reassignment(): @@ -495,7 +580,7 @@ def test_station_diff_asset_reassignment(): ) target_station = start_station.model_copy( update={ - "asset_switching_table": np.array([[False, True], [True, False]]), + "branch_switching_table": np.array([[False, True], [True, False]]), } ) @@ -503,8 +588,10 @@ def test_station_diff_asset_reassignment(): assert realized_station.station == target_station assert realized_station.coupler_diff == [] - assert set(realized_station.reassignment_diff) == set([(0, 0, False), (0, 1, True), (1, 0, True), (1, 1, False)]) - assert realized_station.disconnection_diff == [] + assert set(realized_station.branch_reassignment_diff) == set([(0, 0, False), (0, 1, True), (1, 0, True), (1, 1, False)]) + assert realized_station.injection_reassignment_diff == [] + assert realized_station.branch_disconnection_diff == [] + assert realized_station.injection_disconnection_diff == [] def test_station_diff_asset_disconnection(): @@ -525,7 +612,7 @@ def test_station_diff_asset_disconnection(): ) target_station = start_station.model_copy( update={ - "asset_switching_table": np.array([[False, False], [False, True]]), + "branch_switching_table": np.array([[False, False], [False, True]]), } ) @@ -533,8 +620,10 @@ def test_station_diff_asset_disconnection(): assert realized_station.station == target_station assert realized_station.coupler_diff == [] - assert realized_station.reassignment_diff == [] - assert realized_station.disconnection_diff == [0] + assert realized_station.branch_reassignment_diff == [] + assert realized_station.injection_reassignment_diff == [] + assert realized_station.branch_disconnection_diff == [0] + assert realized_station.injection_disconnection_diff == [] def test_station_diff_unsupported_reconnection(): @@ -555,7 +644,7 @@ def test_station_diff_unsupported_reconnection(): ) target_station = start_station.model_copy( update={ - "asset_switching_table": np.array([[True, False], [False, True]]), + "branch_switching_table": np.array([[True, False], [False, True]]), } ) @@ -585,7 +674,7 @@ def test_topology_diff() -> None: # We perform reassignments and open the coupler target_station_1 = start_station_1.model_copy( update={ - "asset_switching_table": np.array([[False, True], [True, False]]), + "branch_switching_table": np.array([[False, True], [True, False]]), "couplers": [start_station_1.couplers[0].model_copy(update={"open": True})], } ) @@ -613,7 +702,7 @@ def test_topology_diff() -> None: # We disconnect an asset target_station_2 = start_station_2.model_copy( update={ - "asset_switching_table": np.array([[False, False], [False, True], [False, False]]), + "branch_switching_table": np.array([[False, False], [False, True], [False, False]]), } ) @@ -662,10 +751,12 @@ def test_topology_diff() -> None: assert realized_topo.topology == target_topology assert realized_topo.coupler_diff == [("station1", target_station_1.couplers[0])] - assert set(realized_topo.reassignment_diff) == set( + assert set(realized_topo.branch_reassignment_diff) == set( [("station1", 0, 0, False), ("station1", 0, 1, True), ("station1", 1, 0, True), ("station1", 1, 1, False)] ) - assert realized_topo.disconnection_diff == [("station2", 0)] + assert realized_topo.injection_reassignment_diff == [] + assert realized_topo.branch_disconnection_diff == [("station2", 0)] + assert realized_topo.injection_disconnection_diff == [] def test_filter_out_of_service(): @@ -693,10 +784,10 @@ def test_filter_out_of_service(): assert len(filtered_station.couplers) == 0 - assert len(filtered_station.asset_connections) == 1 - assert filtered_station.asset_connections[0].asset.grid_model_id == "line1" + assert len(filtered_station.branch_connections) == 1 + assert filtered_station.branch_connections[0].asset.grid_model_id == "line1" - assert filtered_station.asset_switching_table.shape == (1, 1) + assert filtered_station.branch_switching_table.shape == (1, 1) def test_filter_disconnected_busbars(): @@ -724,7 +815,7 @@ def test_filter_disconnected_busbars(): assert len(removed_busbars) == 1 assert removed_busbars[0].int_id == 3 - assert filtered_station.asset_switching_table.shape == (2, 1) + assert filtered_station.branch_switching_table.shape == (2, 1) def test_filter_disconnected_busbars_open_coupler(): @@ -755,7 +846,7 @@ def test_filter_disconnected_busbars_open_coupler(): assert {busbar.int_id for busbar in filtered_station.busbars} == {1, 2} assert len(removed_busbars) == 1 assert removed_busbars[0].int_id == 3 - assert filtered_station.asset_switching_table.shape == (2, 1) + assert filtered_station.branch_switching_table.shape == (2, 1) def test_reindex_busbars(): @@ -802,7 +893,7 @@ def test_has_transmission_line_switching(): assert has_transmission_line_switching(station) is True - station.asset_switching_table = np.array([[True, False], [False, True]]) + station.branch_switching_table = np.array([[True, False], [False, True]]) assert has_transmission_line_switching(station) is False @@ -840,10 +931,10 @@ def test_order_station_assets() -> None: assert not_found == [] assert ignored == ["line2"] - assert len(ordered.asset_connections) == len(desired_order) - assert [asset_connection.asset.grid_model_id for asset_connection in ordered.asset_connections] == desired_order + assert len(ordered.branch_connections) == len(desired_order) + assert [asset_connection.asset.grid_model_id for asset_connection in ordered.branch_connections] == desired_order assert np.array_equal( - ordered.asset_switching_table, + ordered.branch_switching_table, np.array( [ [False, False, True, True], @@ -858,8 +949,8 @@ def test_order_station_assets() -> None: assert not_found == ["pink_unicorn"] assert ignored == ["line2"] - assert len(ordered.asset_connections) == len(desired_order) - 1 - assert [asset_connection.asset.grid_model_id for asset_connection in ordered.asset_connections] == [ + assert len(ordered.branch_connections) == len(desired_order) - 1 + assert [asset_connection.asset.grid_model_id for asset_connection in ordered.branch_connections] == [ "line5", "line4", "line3", @@ -982,8 +1073,8 @@ def test_fuse_coupler(): assert len(fused_station.couplers) == 1 assert fused_station.couplers[0].grid_model_id == "coupler2" expected_switching_table = np.array([[True, True, True, False], [False, False, False, True]]) - assert np.array_equal(fused_station.asset_switching_table, expected_switching_table) - assert np.array_equal(fused_station.asset_connectivity, expected_switching_table) + assert np.array_equal(fused_station.branch_switching_table, expected_switching_table) + assert np.array_equal(fused_station.branch_connectivity, expected_switching_table) # Test fusing the coupler with copy_info_from=False fused_station = fuse_coupler(station, "coupler1", copy_info_from=False) @@ -993,8 +1084,8 @@ def test_fuse_coupler(): assert fused_station.busbars[1].grid_model_id == "busbar3" assert len(fused_station.couplers) == 1 assert fused_station.couplers[0].grid_model_id == "coupler2" - assert np.array_equal(fused_station.asset_switching_table, expected_switching_table) - assert np.array_equal(fused_station.asset_connectivity, expected_switching_table) + assert np.array_equal(fused_station.branch_switching_table, expected_switching_table) + assert np.array_equal(fused_station.branch_connectivity, expected_switching_table) # Test fusing the other coupler @@ -1005,8 +1096,8 @@ def test_fuse_coupler(): assert len(fused_station.couplers) == 1 assert fused_station.couplers[0].grid_model_id == "coupler1" expected_switching_table = np.array([[True, False, False, False], [False, True, True, True]]) - assert np.array_equal(fused_station.asset_switching_table, expected_switching_table) - assert np.array_equal(fused_station.asset_connectivity, expected_switching_table) + assert np.array_equal(fused_station.branch_switching_table, expected_switching_table) + assert np.array_equal(fused_station.branch_connectivity, expected_switching_table) fused_station = fuse_coupler(station, "coupler2", copy_info_from=False) assert len(fused_station.busbars) == 2 @@ -1015,8 +1106,8 @@ def test_fuse_coupler(): assert len(fused_station.couplers) == 1 assert fused_station.couplers[0].grid_model_id == "coupler1" expected_switching_table = np.array([[True, False, False, False], [False, True, True, True]]) - assert np.array_equal(fused_station.asset_switching_table, expected_switching_table) - assert np.array_equal(fused_station.asset_connectivity, expected_switching_table) + assert np.array_equal(fused_station.branch_switching_table, expected_switching_table) + assert np.array_equal(fused_station.branch_connectivity, expected_switching_table) # Test fusing a non-existent coupler with pytest.raises(ValueError, match="Coupler invalid_coupler not found in station station1"): @@ -1033,11 +1124,41 @@ def test_fuse_all_couplers_with_type(): Busbar(int_id=5, grid_model_id="busbar5"), ], couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1", type="BREAKER"), - BusbarCoupler(busbar_from_id=2, busbar_to_id=3, open=False, grid_model_id="coupler2", type="BREAKER"), - BusbarCoupler(busbar_from_id=3, busbar_to_id=4, open=False, grid_model_id="coupler3", type="DISCONNECTOR"), - BusbarCoupler(busbar_from_id=4, busbar_to_id=5, open=False, grid_model_id="coupler4", type="BREAKER"), - BusbarCoupler(busbar_from_id=5, busbar_to_id=4, open=False, grid_model_id="coupler5", type="DISCONNECTOR"), + BusbarCoupler( + busbar_from_id=1, + busbar_to_id=2, + open=False, + grid_model_id="coupler1", + coupler_type="BREAKER", + ), + BusbarCoupler( + busbar_from_id=2, + busbar_to_id=3, + open=False, + grid_model_id="coupler2", + coupler_type="BREAKER", + ), + BusbarCoupler( + busbar_from_id=3, + busbar_to_id=4, + open=False, + grid_model_id="coupler3", + coupler_type="DISCONNECTOR", + ), + BusbarCoupler( + busbar_from_id=4, + busbar_to_id=5, + open=False, + grid_model_id="coupler4", + coupler_type="BREAKER", + ), + BusbarCoupler( + busbar_from_id=5, + busbar_to_id=4, + open=False, + grid_model_id="coupler5", + coupler_type="DISCONNECTOR", + ), ], assets=[ SwitchableAsset(grid_model_id="line1"), @@ -1081,8 +1202,8 @@ def test_fuse_all_couplers_with_type(): [False, False, False], ] ) - assert np.array_equal(fused_station.asset_switching_table, expected_switching_table) - assert np.array_equal(fused_station.asset_connectivity, expected_switching_table) + assert np.array_equal(fused_station.branch_switching_table, expected_switching_table) + assert np.array_equal(fused_station.branch_connectivity, expected_switching_table) # Test fusing all couplers of type DISCONNECTOR fused_station, fused_couplers = fuse_all_couplers_with_type(station, "DISCONNECTOR", copy_info_from=True) @@ -1135,8 +1256,20 @@ def test_filter_duplicate_couplers_with_type_hierarchy(): Busbar(int_id=2, grid_model_id="busbar2"), ], couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1", type="DISCONNECTOR"), - BusbarCoupler(busbar_from_id=2, busbar_to_id=1, open=False, grid_model_id="coupler2", type="BREAKER"), + BusbarCoupler( + busbar_from_id=1, + busbar_to_id=2, + open=False, + grid_model_id="coupler1", + coupler_type="DISCONNECTOR", + ), + BusbarCoupler( + busbar_from_id=2, + busbar_to_id=1, + open=False, + grid_model_id="coupler2", + coupler_type="BREAKER", + ), ], assets=[ SwitchableAsset(grid_model_id="line1"), @@ -1152,11 +1285,11 @@ def test_filter_duplicate_couplers_with_type_hierarchy(): assert len(filtered_station.couplers) == 1 assert filtered_station.couplers[0].grid_model_id == "coupler2" - assert filtered_station.couplers[0].type == "BREAKER" + assert filtered_station.couplers[0].coupler_type == "BREAKER" assert len(removed_couplers) == 1 assert removed_couplers[0].grid_model_id == "coupler1" - assert removed_couplers[0].type == "DISCONNECTOR" + assert removed_couplers[0].coupler_type == "DISCONNECTOR" # Test with reversed hierarchy where DISCONNECTOR is preferred filtered_station, removed_couplers = filter_duplicate_couplers( @@ -1165,11 +1298,11 @@ def test_filter_duplicate_couplers_with_type_hierarchy(): assert len(filtered_station.couplers) == 1 assert filtered_station.couplers[0].grid_model_id == "coupler1" - assert filtered_station.couplers[0].type == "DISCONNECTOR" + assert filtered_station.couplers[0].coupler_type == "DISCONNECTOR" assert len(removed_couplers) == 1 assert removed_couplers[0].grid_model_id == "coupler2" - assert removed_couplers[0].type == "BREAKER" + assert removed_couplers[0].coupler_type == "BREAKER" def test_filter_duplicate_couplers_no_duplicates(): @@ -1203,8 +1336,20 @@ def test_filter_duplicate_couplers_with_unknown_type(): Busbar(int_id=2, grid_model_id="busbar2"), ], couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1", type="KNOWN"), - BusbarCoupler(busbar_from_id=2, busbar_to_id=1, open=False, grid_model_id="coupler2", type="UNKNOWN"), + BusbarCoupler( + busbar_from_id=1, + busbar_to_id=2, + open=False, + grid_model_id="coupler1", + coupler_type="KNOWN", + ), + BusbarCoupler( + busbar_from_id=2, + busbar_to_id=1, + open=False, + grid_model_id="coupler2", + coupler_type="UNKNOWN", + ), ], assets=[ SwitchableAsset(grid_model_id="line1"), @@ -1218,11 +1363,11 @@ def test_filter_duplicate_couplers_with_unknown_type(): assert len(filtered_station.couplers) == 1 assert filtered_station.couplers[0].grid_model_id == "coupler1" - assert filtered_station.couplers[0].type == "KNOWN" + assert filtered_station.couplers[0].coupler_type == "KNOWN" assert len(removed_couplers) == 1 assert removed_couplers[0].grid_model_id == "coupler2" - assert removed_couplers[0].type == "UNKNOWN" + assert removed_couplers[0].coupler_type == "UNKNOWN" def test_filter_duplicate_couplers_multiple_duplicates(): @@ -1232,9 +1377,27 @@ def test_filter_duplicate_couplers_multiple_duplicates(): Busbar(int_id=2, grid_model_id="busbar2"), ], couplers=[ - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=False, grid_model_id="coupler1", type="TYPE_A"), - BusbarCoupler(busbar_from_id=2, busbar_to_id=1, open=False, grid_model_id="coupler2", type="TYPE_B"), - BusbarCoupler(busbar_from_id=1, busbar_to_id=2, open=True, grid_model_id="coupler3", type="TYPE_C"), + BusbarCoupler( + busbar_from_id=1, + busbar_to_id=2, + open=False, + grid_model_id="coupler1", + coupler_type="TYPE_A", + ), + BusbarCoupler( + busbar_from_id=2, + busbar_to_id=1, + open=False, + grid_model_id="coupler2", + coupler_type="TYPE_B", + ), + BusbarCoupler( + busbar_from_id=1, + busbar_to_id=2, + open=True, + grid_model_id="coupler3", + coupler_type="TYPE_C", + ), ], assets=[ SwitchableAsset(grid_model_id="line1"), @@ -1250,7 +1413,7 @@ def test_filter_duplicate_couplers_multiple_duplicates(): assert len(filtered_station.couplers) == 1 assert filtered_station.couplers[0].grid_model_id == "coupler2" - assert filtered_station.couplers[0].type == "TYPE_B" + assert filtered_station.couplers[0].coupler_type == "TYPE_B" assert len(removed_couplers) == 2 assert set(c.grid_model_id for c in removed_couplers) == {"coupler1", "coupler3"} diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index e02315165..131d83320 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -13,8 +13,10 @@ from fsspec.implementations.dirfs import DirFileSystem from pydantic import ValidationError from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, + InjectionAsset, MaterializedAssetConnection, MaterializedStation, RawStation, @@ -42,6 +44,8 @@ def build_raw_station( couplers: list[BusbarCoupler], asset_ids: list[str], asset_switching_table: np.ndarray, + injection_asset_ids: list[str] | None = None, + injection_switching_table: np.ndarray | None = None, asset_terminals: list[str | None] | None = None, asset_bay_ids: list[str | None] | None = None, ) -> RawStation: @@ -69,6 +73,13 @@ def build_raw_station( RawStation Raw station representation suitable for topology construction in tests. """ + resolved_injection_asset_ids = injection_asset_ids if injection_asset_ids is not None else [] + resolved_injection_switching_table = ( + injection_switching_table + if injection_switching_table is not None + else np.zeros((len(busbars), len(resolved_injection_asset_ids)), dtype=bool) + ) + return RawStation( grid_model_id=grid_model_id, name=None, @@ -77,7 +88,7 @@ def build_raw_station( voltage_level=None, busbars=busbars, couplers=couplers, - asset_connections=[ + branch_connections=[ StationAssetConnection(asset_id=asset_id, terminal=asset_terminal, asset_bay_id=asset_bay_id) for asset_id, asset_terminal, asset_bay_id in zip( asset_ids, @@ -86,8 +97,53 @@ def build_raw_station( strict=True, ) ], - asset_switching_table=asset_switching_table, - asset_connectivity=None, + injection_connections=[ + StationAssetConnection(asset_id=asset_id, terminal=None, asset_bay_id=None) + for asset_id in resolved_injection_asset_ids + ], + branch_switching_table=asset_switching_table, + injection_switching_table=resolved_injection_switching_table, + branch_connectivity=None, + injection_connectivity=None, + model_log=None, + ) + + +def build_materialized_station( + grid_model_id: str, + busbars: list[Busbar], + couplers: list[BusbarCoupler], + assets: list[SwitchableAsset], + asset_switching_table: np.ndarray, + injection_assets: list[SwitchableAsset] | None = None, + injection_switching_table: np.ndarray | None = None, +) -> MaterializedStation: + """Build a materialized station using split branch/injection fields.""" + resolved_injection_assets = injection_assets if injection_assets is not None else [] + resolved_injection_switching_table = ( + injection_switching_table + if injection_switching_table is not None + else np.zeros((len(busbars), len(resolved_injection_assets)), dtype=bool) + ) + return MaterializedStation.model_construct( + grid_model_id=grid_model_id, + name=None, + type=None, + region=None, + voltage_level=None, + busbars=busbars, + couplers=couplers, + branch_connections=[ + MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + ], + injection_connections=[ + MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) + for asset in resolved_injection_assets + ], + branch_switching_table=asset_switching_table, + injection_switching_table=resolved_injection_switching_table, + branch_connectivity=None, + injection_connectivity=None, model_log=None, ) @@ -174,12 +230,18 @@ def test_store_and_load_station_diff_io_random_roundtrip(tmp_path: Path): n_actions = int(rng.integers(1, 8)) n_couplers = int(rng.integers(1, 6)) n_busbars = int(rng.integers(1, 6)) - n_assets = int(rng.integers(1, 10)) + n_branch_assets = int(rng.integers(1, 10)) + n_injection_assets = int(rng.integers(0, 6)) station_diffs.append( StationDiffArray( grid_model_id=f"station_{station_idx}", coupler_open=rng.integers(0, 2, size=(n_actions, n_couplers), dtype=np.uint8).astype(bool), - switching_table=rng.integers(0, 2, size=(n_actions, n_busbars, n_assets), dtype=np.uint8).astype(bool), + branch_switching_table=rng.integers( + 0, 2, size=(n_actions, n_busbars, n_branch_assets), dtype=np.uint8 + ).astype(bool), + injection_switching_table=rng.integers( + 0, 2, size=(n_actions, n_busbars, n_injection_assets), dtype=np.uint8 + ).astype(bool), ) ) @@ -194,9 +256,11 @@ def test_store_and_load_station_diff_io_random_roundtrip(tmp_path: Path): result = loaded_by_id[original.grid_model_id] assert result.grid_model_id == original.grid_model_id assert result.coupler_open.dtype == bool - assert result.switching_table.dtype == bool + assert result.branch_switching_table.dtype == bool + assert result.injection_switching_table.dtype == bool assert np.array_equal(result.coupler_open, original.coupler_open) - assert np.array_equal(result.switching_table, original.switching_table) + assert np.array_equal(result.branch_switching_table, original.branch_switching_table) + assert np.array_equal(result.injection_switching_table, original.injection_switching_table) def test_store_and_load_station_diff_io_empty_list(tmp_path: Path): @@ -211,7 +275,8 @@ def test_station_diff_array_raises_for_mismatched_action_count() -> None: StationDiffArray( grid_model_id="station_1", coupler_open=np.zeros((5, 1), dtype=bool), - switching_table=np.zeros((10, 2, 7), dtype=bool), + branch_switching_table=np.zeros((10, 2, 7), dtype=bool), + injection_switching_table=np.zeros((10, 2, 0), dtype=bool), ) @@ -221,12 +286,14 @@ def test_store_and_load_station_diff_io_supports_different_station_action_counts StationDiffArray( grid_model_id="station_1", coupler_open=np.zeros((5, 1), dtype=bool), - switching_table=np.zeros((5, 2, 7), dtype=bool), + branch_switching_table=np.zeros((5, 2, 7), dtype=bool), + injection_switching_table=np.zeros((5, 2, 1), dtype=bool), ), StationDiffArray( grid_model_id="station_2", coupler_open=np.zeros((10, 2), dtype=bool), - switching_table=np.zeros((10, 3, 4), dtype=bool), + branch_switching_table=np.zeros((10, 3, 4), dtype=bool), + injection_switching_table=np.zeros((10, 3, 2), dtype=bool), ), ] @@ -237,7 +304,8 @@ def test_store_and_load_station_diff_io_supports_different_station_action_counts ("station_1", (5, 1)), ("station_2", (10, 2)), ] - assert [station_diff.switching_table.shape for station_diff in loaded] == [(5, 2, 7), (10, 3, 4)] + assert [station_diff.branch_switching_table.shape for station_diff in loaded] == [(5, 2, 7), (10, 3, 4)] + assert [station_diff.injection_switching_table.shape for station_diff in loaded] == [(5, 2, 1), (10, 3, 2)] def test_store_and_load_station_diff_io_preserves_station_order(tmp_path: Path) -> None: @@ -246,17 +314,20 @@ def test_store_and_load_station_diff_io_preserves_station_order(tmp_path: Path) StationDiffArray( grid_model_id="station_10", coupler_open=np.zeros((1, 1), dtype=bool), - switching_table=np.zeros((1, 1, 1), dtype=bool), + branch_switching_table=np.zeros((1, 1, 1), dtype=bool), + injection_switching_table=np.zeros((1, 1, 0), dtype=bool), ), StationDiffArray( grid_model_id="station_2", coupler_open=np.zeros((1, 1), dtype=bool), - switching_table=np.zeros((1, 1, 1), dtype=bool), + branch_switching_table=np.zeros((1, 1, 1), dtype=bool), + injection_switching_table=np.zeros((1, 1, 0), dtype=bool), ), StationDiffArray( grid_model_id="station_1", coupler_open=np.zeros((1, 1), dtype=bool), - switching_table=np.zeros((1, 1, 1), dtype=bool), + branch_switching_table=np.zeros((1, 1, 1), dtype=bool), + injection_switching_table=np.zeros((1, 1, 0), dtype=bool), ), ] @@ -279,9 +350,12 @@ def test_validate_actions_grouped_accepts_grouped_actions(): voltage_level=None, busbars=[], couplers=[], - asset_connections=[], - asset_switching_table=np.zeros((0, 0), dtype=bool), - asset_connectivity=None, + branch_connections=[], + injection_connections=[], + branch_switching_table=np.zeros((0, 0), dtype=bool), + injection_switching_table=np.zeros((0, 0), dtype=bool), + branch_connectivity=None, + injection_connectivity=None, model_log=None, ) station_s2 = station_s1.model_copy(update={"grid_model_id": "s2"}) @@ -300,9 +374,12 @@ def test_validate_actions_grouped_raises_for_non_grouped_actions(): voltage_level=None, busbars=[], couplers=[], - asset_connections=[], - asset_switching_table=np.zeros((0, 0), dtype=bool), - asset_connectivity=None, + branch_connections=[], + injection_connections=[], + branch_switching_table=np.zeros((0, 0), dtype=bool), + injection_switching_table=np.zeros((0, 0), dtype=bool), + branch_connectivity=None, + injection_connectivity=None, model_log=None, ) station_s2 = station_s1.model_copy(update={"grid_model_id": "s2"}) @@ -340,18 +417,21 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - asset_connections=[MaterializedAssetConnection.model_construct(asset=assets[0], terminal=None, asset_bay=None)], - asset_switching_table=np.zeros((1, 1), dtype=bool), - asset_connectivity=None, + branch_connections=[MaterializedAssetConnection.model_construct(asset=assets[0], terminal=None, asset_bay=None)], + injection_connections=[], + branch_switching_table=np.zeros((1, 1), dtype=bool), + injection_switching_table=np.zeros((1, 0), dtype=bool), + branch_connectivity=None, + injection_connectivity=None, model_log=None, ) station_b = station_a.model_copy( update={ "grid_model_id": "station_b", - "asset_connections": [ - station_a.asset_connections[0].model_copy( + "branch_connections": [ + station_a.branch_connections[0].model_copy( update={ - "asset": station_a.asset_connections[0].asset.model_copy( + "asset": station_a.branch_connections[0].asset.model_copy( update={"grid_model_id": "station_b_asset_0"} ) } @@ -373,9 +453,12 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - asset_connections=[StationAssetConnection(asset_id="station_a_asset_0", terminal=None, asset_bay_id=None)], - asset_switching_table=np.zeros((1, 1), dtype=bool), - asset_connectivity=None, + branch_connections=[StationAssetConnection(asset_id="station_a_asset_0", terminal=None, asset_bay_id=None)], + injection_connections=[], + branch_switching_table=np.zeros((1, 1), dtype=bool), + injection_switching_table=np.zeros((1, 0), dtype=bool), + branch_connectivity=None, + injection_connectivity=None, model_log=None, ), RawStation( @@ -386,13 +469,17 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - asset_connections=[StationAssetConnection(asset_id="station_b_asset_0", terminal=None, asset_bay_id=None)], - asset_switching_table=np.zeros((1, 1), dtype=bool), - asset_connectivity=None, + branch_connections=[StationAssetConnection(asset_id="station_b_asset_0", terminal=None, asset_bay_id=None)], + injection_connections=[], + branch_switching_table=np.zeros((1, 1), dtype=bool), + injection_switching_table=np.zeros((1, 0), dtype=bool), + branch_connectivity=None, + injection_connectivity=None, model_log=None, ), ], - assets=[station_a.asset_connections[0].asset, station_b.asset_connections[0].asset], + branch_assets=[station_a.branch_connections[0].asset, station_b.branch_connections[0].asset], + injection_assets=[], asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -417,6 +504,7 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): starting_stations: list[MaterializedStation] = [] starting_raw_stations: list[RawStation] = [] starting_assets: list[SwitchableAsset] = [] + starting_injection_assets: list[SwitchableAsset] = [] actions: list[MaterializedStation] = [] expected_by_station: dict[str, list[MaterializedStation]] = {} @@ -424,7 +512,8 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): for station_idx in range(n_stations): grid_model_id = f"station_{station_idx}" n_busbars = int(rng.integers(2, 5)) - n_assets = int(rng.integers(1, 7)) + n_branch_assets = int(rng.integers(1, 7)) + n_injection_assets = int(rng.integers(0, 4)) n_couplers = int(rng.integers(1, 6)) n_actions = int(rng.integers(1, 6)) @@ -439,14 +528,23 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): ) for busbar_idx in range(n_busbars) ] - assets = [ - SwitchableAsset.model_construct( + branch_assets = [ + BranchAsset.model_construct( grid_model_id=f"{grid_model_id}_asset_{asset_idx}", type=None, name=None, in_service=True, ) - for asset_idx in range(n_assets) + for asset_idx in range(n_branch_assets) + ] + injection_assets = [ + InjectionAsset.model_construct( + grid_model_id=f"{grid_model_id}_inj_{asset_idx}", + type=None, + name=None, + in_service=True, + ) + for asset_idx in range(n_injection_assets) ] starting_couplers = [ @@ -463,32 +561,32 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): for coupler_idx in range(n_couplers) ] - starting_switching_table = rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool) + starting_branch_switching_table = rng.integers(0, 2, size=(n_busbars, n_branch_assets), dtype=np.uint8).astype(bool) + starting_injection_switching_table = rng.integers(0, 2, size=(n_busbars, n_injection_assets), dtype=np.uint8).astype( + bool + ) starting_raw_stations.append( build_raw_station( grid_model_id, busbars, starting_couplers, - [asset.grid_model_id for asset in assets], - starting_switching_table, + [asset.grid_model_id for asset in branch_assets], + starting_branch_switching_table, + injection_asset_ids=[asset.grid_model_id for asset in injection_assets], + injection_switching_table=starting_injection_switching_table, ) ) - starting_assets.extend(assets) + starting_assets.extend(branch_assets) + starting_injection_assets.extend(injection_assets) - starting_station = MaterializedStation.model_construct( + starting_station = build_materialized_station( grid_model_id=grid_model_id, - name=None, - type=None, - region=None, - voltage_level=None, busbars=busbars, couplers=starting_couplers, - asset_connections=[ - MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets - ], - asset_switching_table=starting_switching_table, - asset_connectivity=None, - model_log=None, + assets=branch_assets, + asset_switching_table=starting_branch_switching_table, + injection_assets=injection_assets, + injection_switching_table=starting_injection_switching_table, ) starting_stations.append(starting_station) @@ -497,12 +595,14 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): couplers = [ coupler.model_copy(update={"open": bool(rng.integers(0, 2))}) for coupler in starting_station.couplers ] - switching_table = rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool) + branch_switching_table = rng.integers(0, 2, size=(n_busbars, n_branch_assets), dtype=np.uint8).astype(bool) + injection_switching_table = rng.integers(0, 2, size=(n_busbars, n_injection_assets), dtype=np.uint8).astype(bool) station_actions.append( starting_station.model_copy( update={ "couplers": couplers, - "asset_switching_table": switching_table, + "branch_switching_table": branch_switching_table, + "injection_switching_table": injection_switching_table, } ) ) @@ -515,7 +615,8 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): grid_model_file=None, name=None, raw_stations=starting_raw_stations, - assets=starting_assets, + branch_assets=starting_assets, + injection_assets=starting_injection_assets, asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -538,7 +639,8 @@ def test_compress_and_expand_station_diffs_random_roundtrip(): expected_coupler_open = [coupler.open for coupler in expected_action.couplers] result_coupler_open = [coupler.open for coupler in result_action.couplers] assert result_coupler_open == expected_coupler_open - assert np.array_equal(result_action.asset_switching_table, expected_action.asset_switching_table) + assert np.array_equal(result_action.branch_switching_table, expected_action.branch_switching_table) + assert np.array_equal(result_action.injection_switching_table, expected_action.injection_switching_table) def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): @@ -601,26 +703,19 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): asset_switching_table, ) ], - assets=assets, + branch_assets=assets, + injection_assets=[], asset_setpoints=None, timestamp=datetime.now(), metrics=None, ) - starting_station = MaterializedStation.model_construct( + starting_station = build_materialized_station( grid_model_id="station_x", - name=None, - type=None, - region=None, - voltage_level=None, busbars=busbars, couplers=couplers, - asset_connections=[ - MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets - ], + assets=assets, asset_switching_table=asset_switching_table, - asset_connectivity=None, - model_log=None, ) valid_action = starting_station.model_copy( @@ -628,18 +723,29 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): "couplers": [ starting_station.couplers[0].model_copy(update={"open": True}), ], - "asset_switching_table": np.array([[False, True], [True, False]], dtype=bool), + "branch_switching_table": np.array([[False, True], [True, False]], dtype=bool), } ) - invalid_action = starting_station.model_copy( - update={ + invalid_action_payload = starting_station.model_dump(mode="python", round_trip=True) + invalid_action_payload.update( + { "couplers": [ - starting_station.couplers[0].model_copy(update={"busbar_from_id": 99}), + BusbarCoupler.model_construct( + grid_model_id=starting_station.couplers[0].grid_model_id, + type=starting_station.couplers[0].coupler_type, + name=starting_station.couplers[0].name, + busbar_from_id=99, + busbar_to_id=starting_station.couplers[0].busbar_to_id, + open=starting_station.couplers[0].open, + in_service=starting_station.couplers[0].in_service, + asset_bay=starting_station.couplers[0].asset_bay, + ) ], - "asset_switching_table": np.array([[True, True], [False, False]], dtype=bool), + "branch_switching_table": np.array([[True, True], [False, False]], dtype=bool), } ) + invalid_action = MaterializedStation.model_construct(**invalid_action_payload) actions = [valid_action, invalid_action] @@ -711,32 +817,25 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): asset_switching_table, ) ], - assets=assets, + branch_assets=assets, + injection_assets=[], asset_setpoints=None, timestamp=datetime.now(), metrics=None, ) - starting_station = MaterializedStation.model_construct( + starting_station = build_materialized_station( grid_model_id="station1", - name=None, - type=None, - region=None, - voltage_level=None, busbars=busbars, couplers=couplers, - asset_connections=[ - MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets - ], + assets=assets, asset_switching_table=asset_switching_table, - asset_connectivity=None, - model_log=None, ) local_action = starting_station.model_copy( update={ "couplers": [starting_station.couplers[0].model_copy(update={"open": True})], - "asset_switching_table": np.array([[False, True], [True, False]], dtype=bool), + "branch_switching_table": np.array([[False, True], [True, False]], dtype=bool), } ) action_set = ActionSet.model_construct( @@ -763,6 +862,6 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): assert loaded_action.grid_model_id == local_action.grid_model_id assert [c.open for c in loaded_action.couplers] == [c.open for c in local_action.couplers] assert np.array_equal( - np.asarray(loaded_action.asset_switching_table), - np.asarray(local_action.asset_switching_table), + np.asarray(loaded_action.branch_switching_table), + np.asarray(local_action.branch_switching_table), ) diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index 0b9ad62ff..1c8171619 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -13,8 +13,10 @@ import pytest from fsspec.implementations.local import LocalFileSystem from toop_engine_interfaces.asset_topology import ( + BranchAsset, Busbar, BusbarCoupler, + MaterializedAssetConnection, MaterializedStation, RawStation, StationAssetConnection, @@ -90,7 +92,7 @@ def _build_large_random_action_set( ] assets = [ - SwitchableAsset.model_construct( + BranchAsset.model_construct( grid_model_id=f"{grid_model_id}_asset_{asset_idx}", type=None, name=None, @@ -99,7 +101,8 @@ def _build_large_random_action_set( for asset_idx in range(n_assets) ] - asset_switching_table = rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool) + branch_switching_table = rng.integers(0, 2, size=(n_busbars, n_assets), dtype=np.uint8).astype(bool) + injection_switching_table = np.zeros((n_busbars, 0), dtype=bool) raw_station = RawStation.model_construct( grid_model_id=grid_model_id, @@ -109,7 +112,7 @@ def _build_large_random_action_set( voltage_level=None, busbars=busbars, couplers=couplers, - asset_connections=[ + branch_connections=[ StationAssetConnection.model_construct( asset_id=asset.grid_model_id, terminal=None, @@ -117,8 +120,11 @@ def _build_large_random_action_set( ) for asset in assets ], - asset_switching_table=asset_switching_table, - asset_connectivity=None, + branch_switching_table=branch_switching_table, + branch_connectivity=None, + injection_connections=[], + injection_switching_table=injection_switching_table, + injection_connectivity=None, model_log=None, ) raw_stations.append(raw_station) @@ -132,13 +138,18 @@ def _build_large_random_action_set( voltage_level=None, busbars=busbars, couplers=couplers, - assets=assets, - asset_switching_table=asset_switching_table, - asset_connectivity=None, + branch_connections=[ + MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + ], + branch_switching_table=branch_switching_table, + branch_connectivity=None, + injection_connections=[], + injection_switching_table=injection_switching_table, + injection_connectivity=None, model_log=None, ) - base_switching_table = np.asarray(starting_station.asset_switching_table, dtype=bool) + base_switching_table = np.asarray(starting_station.branch_switching_table, dtype=bool) for _ in range(actions_per_station): action_couplers = [ coupler.model_copy(update={"open": bool(rng.integers(0, 2))}) for coupler in starting_station.couplers @@ -154,7 +165,7 @@ def _build_large_random_action_set( starting_station.model_copy( update={ "couplers": action_couplers, - "asset_switching_table": switching_table, + "branch_switching_table": switching_table, } ) ) @@ -164,7 +175,8 @@ def _build_large_random_action_set( grid_model_file=None, name=None, raw_stations=raw_stations, - assets=topology_assets, + branch_assets=topology_assets, + injection_assets=[], asset_setpoints=None, timestamp=datetime.now(), metrics=None, @@ -210,7 +222,7 @@ def test_stored_action_set_large_performance(tmp_path: Path, record_property) -> starting_stations = action_set.starting_topology.materialize_stations() assert len(starting_stations) == n_stations assert len(action_set.local_actions) == n_actions - mean_assets = float(np.mean([len(station.assets) for station in starting_stations])) + mean_assets = float(np.mean([len(station.branch_connections) for station in starting_stations])) assert mean_assets == avg_assets_per_station assert all(len(station.couplers) == couplers_per_station for station in starting_stations) 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 204f3dd8f..2f91abd83 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 @@ -10,10 +10,8 @@ from datetime import datetime, timedelta from functools import partial from pathlib import Path -from typing import cast import numpy as np -import pandera.typing.polars as patpl import pypowsybl import structlog from beartype.typing import Callable, Optional @@ -32,7 +30,7 @@ from toop_engine_interfaces.filesystem_helper import load_pydantic_model_fs from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.loadflow_result_helpers_polars import load_loadflow_results_polars, save_loadflow_results_polars -from toop_engine_interfaces.loadflow_results_polars import BranchResultSchemaPolars, LoadflowResultsPolars +from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.messages.lf_service.loadflow_results import StoredLoadflowReference from toop_engine_interfaces.nminus1_definition import Nminus1Definition from toop_engine_interfaces.stored_action_set import ActionSet, load_action_set_fs @@ -96,7 +94,7 @@ def update_initial_metrics_with_worst_k_contingencies( The number of worst contingencies to consider for the initial metrics. """ case_ids, top_k_overloads_n_1 = get_worst_k_contingencies_ac( - cast("patpl.LazyFrame[BranchResultSchemaPolars]", initial_loadflow.branch_results), + initial_loadflow.branch_results, k=worst_k, ) 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 1e25121f7..c8e7ea7d7 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 @@ -166,7 +166,7 @@ def extract_switching_distance(additional_info: AdditionalActionInfo) -> int: The switching distance, or 0 if not available """ if isinstance(additional_info, RealizedTopology): - return len(additional_info.reassignment_diff) + return len(additional_info.branch_reassignment_diff) + len(additional_info.injection_reassignment_diff) if isinstance(additional_info, pd.DataFrame): return len(additional_info) raise ValueError("Unknown format for additional info") diff --git a/toop-engine-benchmark/assess_benchmarks.py b/toop-engine-benchmark/assess_benchmarks.py index a5c6ebb37..f4d24965d 100644 --- a/toop-engine-benchmark/assess_benchmarks.py +++ b/toop-engine-benchmark/assess_benchmarks.py @@ -21,7 +21,7 @@ import statistics as stats from pathlib import Path -from beartype.typing import Any, Iterable, cast +from beartype.typing import Any, Iterable # Disable beartype claw for this module to avoid decorating Hydra's generated main wrapper __beartype__ = False @@ -114,7 +114,7 @@ def _load_overrides(run_root: Path) -> tuple[list[str], dict[str, Any]]: resolved_cfg = OmegaConf.from_cli(overrides_list) container = OmegaConf.to_container(resolved_cfg, resolve=True) if isinstance(container, dict): - overrides_resolved = cast(dict[str, Any], container) + overrides_resolved = container except Exception as exc: # pragma: no cover - defensive logging logger.info(f"Failed to parse overrides for {overrides_file}: {exc}") From 93dfc3662b37dedb6d9d613573471f9679a776ea Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Fri, 19 Jun 2026 12:23:16 +0200 Subject: [PATCH 15/24] bad merge Signed-off-by: Leonard Hilfrich --- .../src/toop_engine_dc_solver/preprocess/network_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fb15cfe86..808a8f73c 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 @@ -739,13 +739,13 @@ def extract_nminus1_definition(network_data: NetworkData) -> Nminus1Definition: ) monitored_nodes = [ MonitoredElement(id=busbar.grid_model_id, name=busbar.name or "", type=busbar.type, kind="bus") - for station in asset_topology.stations + for station in asset_topology.materialize_stations() for busbar in station.busbars ] monitored_switches = [ MonitoredElement(id=switch.grid_model_id, name=switch.name or "", type=switch.type, kind="switch") - for station in asset_topology.stations + for station in asset_topology.materialize_stations() for switch in station.couplers ] From 978e8fa4340eb3ee7d286c897d5df7bffc3565ef Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Fri, 19 Jun 2026 12:24:43 +0200 Subject: [PATCH 16/24] docsfix Signed-off-by: Leonard Hilfrich --- docs/interfaces/asset_topology.md | 2 +- packages/grid_helpers_pkg/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/interfaces/asset_topology.md b/docs/interfaces/asset_topology.md index da026f397..e01f89445 100644 --- a/docs/interfaces/asset_topology.md +++ b/docs/interfaces/asset_topology.md @@ -54,7 +54,7 @@ Asset Topology is essential when stations do not allow free assignment of lines - [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] Represents an asset with a setpoint (e.g., PST or HVDC). -- [`RealizedStation`][toop_engine_interfaces.asset_topology.RealizedStation] +- [`AppliedStation`][toop_engine_interfaces.asset_topology.AppliedStation] Contains a station and the changes made to it. - [`RealizedTopology`][toop_engine_interfaces.asset_topology.RealizedTopology] diff --git a/packages/grid_helpers_pkg/README.md b/packages/grid_helpers_pkg/README.md index 3ae50f356..0227da564 100644 --- a/packages/grid_helpers_pkg/README.md +++ b/packages/grid_helpers_pkg/README.md @@ -29,7 +29,7 @@ Grid Helpers acts as the foundational abstraction layer that standardizes operat ## PyPowSyBl Key Data Structures and Concepts - **Loadflow parameter**:A collection of loadflow parameter used by the ToOp project. - **Helpers**: A standardized function set to get grid information. Heavy used by the [`PowsyblBackend`][toop_engine_dc_solver.preprocess.powsybl.powsybl_backend.PowsyblBackend]. -- **Asset Topology helpers**: An implementation of the [AssetTopology][toop_engine_interfaces.asset_topology], main entry: [`get_list_of_stations`][toop_engine_grid_helpers.powsybl.powsybl_asset_topo.get_list_of_stations] +- **Asset Topology helpers**: An implementation of the [AssetTopology][toop_engine_interfaces.asset_topology], main entry: [`get_topology`][toop_engine_grid_helpers.powsybl.powsybl_asset_topo.get_topology] - **Single line diagram (SLD)**: A modified version of the powsybl SLD, with a bright and dark mode. This could be integrated into powsybl itself as it's own package (java). - **Polars DataFrame**: Once you have millions of rows in you dataframe, the PowSyBl (Java) to PyPowSybl converter (Java->C++->Python->Pandas) becomes slow. You can eliminate one large bottleneck by removing pandas from this data extraction path. Ideally the transfer from Java to PyPowSyBl would deliver in this case parquet as a format. The speed boost only starts with large amount of N-1 cases: 3000 Bus network with more than 500 N-1 cases. Below that the speed of collection is roughly equal. From b70a0816fc644704e547df5347325cd09ada12a7 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 22 Jun 2026 15:18:20 +0200 Subject: [PATCH 17/24] feat: circuitgroupclasse + file refactor Signed-off-by: Leonard Hilfrich --- .../toop_engine_dc_solver/example_classes.py | 9 +- .../toop_engine_dc_solver/example_grids.py | 12 +- .../export/disconnection_switch_updates.py | 4 +- .../toop_engine_dc_solver/export/export.py | 3 +- .../export/station_switch_updates.py | 3 +- .../postprocess/abstract_runner.py | 2 +- .../apply_asset_topo_pandapower.py | 12 +- .../postprocess/apply_asset_topo_powsybl.py | 11 +- .../postprocess/postprocess_pandapower.py | 6 +- .../postprocess/postprocess_powsybl.py | 4 +- .../postprocess/realize_assignment.py | 2 +- .../preprocess/action_set.py | 2 +- .../preprocess/network_data.py | 3 +- .../pandapower/pandapower_backend.py | 2 +- .../preprocess/powsybl/powsybl_backend.py | 2 +- .../preprocess/preprocess.py | 4 +- .../preprocess/preprocess_bb_outage.py | 10 +- .../preprocess_station_realisations.py | 2 +- .../preprocess/preprocess_switching.py | 14 +- packages/dc_solver_pkg/tests/conftest.py | 12 +- .../tests/deprecated/assignment.py | 2 +- .../export/test_asset_topology_to_dgs.py | 2 +- .../test_disconnection_switch_updates.py | 2 +- .../dc_solver_pkg/tests/export/test_export.py | 2 +- .../export/test_station_switch_updates.py | 2 +- .../tests/jax/test_busbar_outage.py | 2 +- .../test_apply_asset_topo_pandapower.py | 2 +- .../test_apply_asset_topo_powsybl.py | 2 +- .../preprocessing/test_loadflows_match.py | 6 +- .../tests/preprocessing/test_preprocess.py | 3 +- .../test_preprocess_bb_outage.py | 6 +- .../test_preprocess_switching.py | 12 +- .../preprocessing/test_realize_assignment.py | 6 +- .../dc_solver_pkg/tests/test_example_grids.py | 2 +- .../powsybl/example_grids.py | 4 +- .../powsybl/powsybl_asset_topo.py | 12 +- packages/grid_helpers_pkg/tests/conftest.py | 2 +- .../tests/powsybl/test_powsybl_asset_topo.py | 7 +- .../exporter/asset_topology_to_ucte.py | 4 +- .../network_graph/graph_to_asset_topo.py | 2 +- .../network_graph_helper_functions.py | 2 +- .../network_graph/powsybl_station_to_graph.py | 10 +- .../pandapower_import/asset_topology.py | 11 +- .../pandapower_import/preprocessing.py | 2 +- .../cgmes/powsybl_masks_cgmes.py | 2 +- .../pypowsybl_import/preprocessing.py | 66 +- packages/importer_pkg/tests/conftest.py | 2 +- .../exporter/test_asset_topology_to_ucte.py | 2 +- .../network_graph/test_graph_to_asset_topo.py | 2 +- .../test_network_graph_helper_functions.py | 2 +- .../test_powsybl_station_to_graph.py | 3 +- .../test_asset_topology_pandapower.py | 7 +- .../test_preprocessing_pandapower.py | 2 +- .../pypowsybl_import/test_preprocessing.py | 7 +- .../toop_engine_interfaces/asset_topology.py | 1441 ----------------- .../asset_topology/__init__.py | 0 .../asset_topology/applied_topology.py | 67 + .../asset_topology/asset_topology.py | 348 ++++ .../asset_topology_helpers.py | 13 +- .../asset_topology/asset_types.py | 53 + .../asset_topology/assets.py | 303 ++++ .../asset_topology/materialized_topology.py | 203 +++ .../asset_topology/station_models.py | 435 +++++ .../asset_topology/topology_conversion.py | 144 ++ .../src/toop_engine_interfaces/backend.py | 2 +- .../messages/lf_service/loadflow_commands.py | 2 +- .../stored_action_set.py | 3 +- .../lf_service/test_loadflow_commands.py | 2 +- .../tests/test_asset_topology.py | 29 +- .../tests/test_asset_topology_helpers.py | 15 +- .../tests/test_filesystem_helpers.py | 2 +- .../tests/test_stored_action_set.py | 13 +- .../test_stored_action_set_performance.py | 12 +- .../ac/scoring_functions.py | 2 +- 74 files changed, 1759 insertions(+), 1651 deletions(-) delete mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/applied_topology.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py rename packages/interfaces_pkg/src/toop_engine_interfaces/{ => asset_topology}/asset_topology_helpers.py (99%) create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_types.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py create mode 100644 packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py index af03bd09d..3259e268c 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py @@ -10,15 +10,12 @@ import datetime import numpy as np -from toop_engine_interfaces.asset_topology import ( - AssetBay, - BranchAsset, - Busbar, - BusbarCoupler, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - StationAssetConnection, Topology, ) +from toop_engine_interfaces.asset_topology.assets import AssetBay, BranchAsset, Busbar, BusbarCoupler +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection def get_basic_node_breaker_topology() -> Topology: 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 452f4c77c..abbea5db0 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 @@ -51,16 +51,12 @@ from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK from toop_engine_grid_helpers.powsybl.powsybl_helpers import save_lf_params_to_fs from toop_engine_importer.pypowsybl_import import preprocessing -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - BusbarCoupler, - InjectionAsset, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, - topology_from_materialized_stations, ) +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, InjectionAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.topology_conversion import topology_from_materialized_stations from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.folder_structure import ( NETWORK_MASK_NAMES, diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py index 389aad4e0..3870f4d9b 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py @@ -11,7 +11,9 @@ import pandera as pa import pandera.typing as pat import structlog -from toop_engine_interfaces.asset_topology import AssetBay, MaterializedStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.assets import AssetBay +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py index 09d07279e..832cbae5c 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/export.py @@ -20,7 +20,8 @@ from toop_engine_dc_solver.export.station_switch_updates import ( get_changing_switches_from_changed_stations, ) -from toop_engine_interfaces.asset_topology import MaterializedStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.stored_action_set import ActionSet diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py index 1b2111e88..b92c21ea5 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py @@ -15,7 +15,8 @@ import pandas as pd import pandera as pa import pandera.typing as pat -from toop_engine_interfaces.asset_topology import MaterializedStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py index a60d7abd4..8f20b28bd 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py @@ -22,7 +22,7 @@ import pandera.typing as pat from beartype.typing import Optional, TypeAlias, Union from fsspec import AbstractFileSystem -from toop_engine_interfaces.asset_topology import RealizedTopology +from toop_engine_interfaces.asset_topology.applied_topology import RealizedTopology from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Nminus1Definition from toop_engine_interfaces.stored_action_set import ActionSet diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py index 5ec1a277f..5c7cbec49 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_pandapower.py @@ -20,15 +20,13 @@ from pandapower.toolbox import element_bus_tuples, get_connected_elements_dict from toop_engine_grid_helpers.pandapower.pandapower_helpers import get_element_table, get_remotely_connected_buses from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import parse_globally_unique_id, table_id -from toop_engine_interfaces.asset_topology import ( - AppliedStation, - Busbar, - BusbarCoupler, - MaterializedStation, - RealizedTopology, +from toop_engine_interfaces.asset_topology.applied_topology import AppliedStation, RealizedTopology +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, ) -from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs, find_busbars_for_coupler +from toop_engine_interfaces.asset_topology.asset_topology_helpers import accumulate_diffs, find_busbars_for_coupler +from toop_engine_interfaces.asset_topology.assets import Busbar, BusbarCoupler +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation logger = structlog.get_logger(__name__) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index 85d19897c..71614cba4 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -26,16 +26,15 @@ from beartype.typing import Literal, Optional, Union from pypowsybl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import assert_station_in_network -from toop_engine_interfaces.asset_topology import ( - AppliedStation, - BusbarCoupler, - MaterializedStation, +from toop_engine_interfaces.asset_topology.applied_topology import AppliedStation, RealizedTopology +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - RealizedTopology, Topology, copy_topology_with_updates, ) -from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs +from toop_engine_interfaces.asset_topology.asset_topology_helpers import accumulate_diffs +from toop_engine_interfaces.asset_topology.assets import BusbarCoupler +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py index c8cb29f8e..cd7222b0a 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py @@ -32,12 +32,12 @@ from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import ( parse_globally_unique_id, ) -from toop_engine_interfaces.asset_topology import ( +from toop_engine_interfaces.asset_topology.applied_topology import ( AppliedStation, RealizedTopology, - topology_from_materialized_stations, ) -from toop_engine_interfaces.asset_topology_helpers import accumulate_diffs, electrical_components +from toop_engine_interfaces.asset_topology.asset_topology_helpers import accumulate_diffs, electrical_components +from toop_engine_interfaces.asset_topology.topology_conversion import topology_from_materialized_stations from toop_engine_interfaces.loadflow_result_helpers_polars import extract_solver_matrices_polars from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Contingency, Nminus1Definition diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index 171613d1e..ac05bfef5 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -46,8 +46,8 @@ extract_single_injection_loadflow_result, load_powsybl_from_fs, ) -from toop_engine_interfaces.asset_topology import topology_from_materialized_stations -from toop_engine_interfaces.asset_topology_helpers import electrical_components +from toop_engine_interfaces.asset_topology.asset_topology_helpers import electrical_components +from toop_engine_interfaces.asset_topology.topology_conversion import topology_from_materialized_stations from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Contingency, Nminus1Definition from toop_engine_interfaces.stored_action_set import ActionSet diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py index 044ff3489..d2eed3855 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py @@ -17,7 +17,7 @@ from jaxtyping import Array, Bool, Int from toop_engine_dc_solver.preprocess.helpers.switching_distance import per_station_switching_distance from toop_engine_dc_solver.preprocess.preprocess_switching import OptimalSeparationSetInfo -from toop_engine_interfaces.asset_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits logger = structlog.get_logger(__name__) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py index 59fd3af2b..c9edf7a86 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/action_set.py @@ -39,7 +39,7 @@ ) from toop_engine_dc_solver.preprocess.helpers.switching_distance import min_hamming_distance_matrix from toop_engine_dc_solver.preprocess.network_data import NetworkData, get_relevant_stations -from toop_engine_interfaces.asset_topology import RawStation +from toop_engine_interfaces.asset_topology.asset_topology import RawStation from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits logger = structlog.get_logger(__name__) 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 808a8f73c..44877ea0c 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 @@ -17,7 +17,8 @@ from jaxtyping import Bool, Float, Int from toop_engine_dc_solver.preprocess.preprocess_switching import OptimalSeparationSetInfo from toop_engine_grid_helpers.powsybl.powsybl_helpers import load_lf_params_from_fs -from toop_engine_interfaces.asset_topology import MaterializedStation, RawStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import RawStation, Topology +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.nminus1_definition import Contingency, GridElement, MonitoredElement, Nminus1Definition from toop_engine_interfaces.stored_action_set import ActionSet, PSTRange diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/pandapower/pandapower_backend.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/pandapower/pandapower_backend.py index 293e2feaf..d4b81d63b 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/pandapower/pandapower_backend.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/pandapower/pandapower_backend.py @@ -36,7 +36,7 @@ get_trafo3w_ppc_branch_idx, get_trafo3w_ppc_node_idx, ) -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.filesystem_helper import load_numpy_filesystem, load_pydantic_model_fs from toop_engine_interfaces.folder_structure import ( 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 949a0ef94..a007eca6d 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 @@ -28,7 +28,7 @@ ) from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK from toop_engine_grid_helpers.powsybl.powsybl_helpers import load_powsybl_from_fs -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.filesystem_helper import load_numpy_filesystem, load_pydantic_model_fs from toop_engine_interfaces.folder_structure import ( 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 a030f9942..d1f9586c0 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 @@ -72,8 +72,8 @@ make_optimal_separation_set, prepare_for_separation_set, ) -from toop_engine_interfaces.asset_topology import topology_from_materialized_stations -from toop_engine_interfaces.asset_topology_helpers import order_topology +from toop_engine_interfaces.asset_topology.asset_topology_helpers import order_topology +from toop_engine_interfaces.asset_topology.topology_conversion import topology_from_materialized_stations from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.messages.preprocess.preprocess_commands import PreprocessParameters, ReassignmentLimits from toop_engine_interfaces.messages.preprocess.preprocess_heartbeat import ( diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py index 7037a99b8..7b7892c64 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_bb_outage.py @@ -19,14 +19,12 @@ OutageData, get_relevant_stations, ) -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - InjectionAsset, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - SwitchableAsset, ) -from toop_engine_interfaces.asset_topology_helpers import find_station_by_id +from toop_engine_interfaces.asset_topology.asset_topology_helpers import find_station_by_id +from toop_engine_interfaces.asset_topology.assets import BranchAsset, InjectionAsset, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation logger = structlog.get_logger(__name__) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py index 21040aa15..d2fee3094 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_station_realisations.py @@ -19,7 +19,7 @@ NetworkData, get_relevant_stations, ) -from toop_engine_interfaces.asset_topology import RawStation +from toop_engine_interfaces.asset_topology.asset_topology import RawStation from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py index 9f9c860eb..893e91a89 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py @@ -25,18 +25,11 @@ number_connected_components, ) from toop_engine_dc_solver.preprocess.helpers.switching_distance import hamming_distance -from toop_engine_interfaces.asset_topology import ( - AssetInjectionType, - BranchAsset, - Busbar, - BusbarCoupler, - InjectionAsset, - MaterializedStation, - SwitchableAsset, +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, copy_topology_with_updates, ) -from toop_engine_interfaces.asset_topology_helpers import ( +from toop_engine_interfaces.asset_topology.asset_topology_helpers import ( filter_disconnected_busbars, filter_duplicate_couplers, filter_out_of_service, @@ -44,6 +37,9 @@ fuse_all_couplers_with_type, order_station_assets, ) +from toop_engine_interfaces.asset_topology.asset_types import AssetInjectionType +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, InjectionAsset, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation logger = structlog.get_logger(__name__) diff --git a/packages/dc_solver_pkg/tests/conftest.py b/packages/dc_solver_pkg/tests/conftest.py index 4789fe93d..31f56de10 100644 --- a/packages/dc_solver_pkg/tests/conftest.py +++ b/packages/dc_solver_pkg/tests/conftest.py @@ -14,6 +14,9 @@ from copy import deepcopy from pathlib import Path +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation + os.environ.setdefault("RAY_ENABLE_UV_RUN_RUNTIME_ENV", "0") import chex @@ -98,15 +101,10 @@ ) from toop_engine_grid_helpers.powsybl.powsybl_helpers import save_lf_params_to_fs from toop_engine_importer.pypowsybl_import import preprocessing -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - BusbarCoupler, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, ) -from toop_engine_interfaces.asset_topology_helpers import load_asset_topology +from toop_engine_interfaces.asset_topology.asset_topology_helpers import load_asset_topology from toop_engine_interfaces.folder_structure import ( NETWORK_MASK_NAMES, OUTPUT_FILE_NAMES, diff --git a/packages/dc_solver_pkg/tests/deprecated/assignment.py b/packages/dc_solver_pkg/tests/deprecated/assignment.py index a1ffbc616..d7efea900 100644 --- a/packages/dc_solver_pkg/tests/deprecated/assignment.py +++ b/packages/dc_solver_pkg/tests/deprecated/assignment.py @@ -19,7 +19,7 @@ from toop_engine_dc_solver.preprocess.preprocess_switching import ( make_separation_set, ) -from toop_engine_interfaces.asset_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation # TODO: A different rule could take precedent here. I would first check for parallel lines/trafos diff --git a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py index 08f2d91d1..39e0764bf 100644 --- a/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py +++ b/packages/dc_solver_pkg/tests/export/test_asset_topology_to_dgs.py @@ -33,7 +33,7 @@ get_coupler_states_from_busbar_couplers, get_diff_switch_states, ) -from toop_engine_interfaces.asset_topology import BusbarCoupler +from toop_engine_interfaces.asset_topology.assets import BusbarCoupler from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema diff --git a/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py index a6fc19656..bab73c10b 100644 --- a/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_disconnection_switch_updates.py @@ -14,7 +14,7 @@ ) from toop_engine_dc_solver.export.export import get_changing_switches_from_actions from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import get_changing_switches_from_topology -from toop_engine_interfaces.asset_topology import RawStation, Topology, copy_topology_with_updates +from toop_engine_interfaces.asset_topology.asset_topology import RawStation, Topology, copy_topology_with_updates from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema diff --git a/packages/dc_solver_pkg/tests/export/test_export.py b/packages/dc_solver_pkg/tests/export/test_export.py index 360786109..be5d91cda 100644 --- a/packages/dc_solver_pkg/tests/export/test_export.py +++ b/packages/dc_solver_pkg/tests/export/test_export.py @@ -21,7 +21,7 @@ from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import get_changing_switches_from_topology from toop_engine_dc_solver.postprocess.postprocess_powsybl import PowsyblRunner from toop_engine_dc_solver.preprocess.network_data import extract_action_set, extract_nminus1_definition, load_lf_params -from toop_engine_interfaces.asset_topology import RawStation, Topology, copy_topology_with_updates +from toop_engine_interfaces.asset_topology.asset_topology import RawStation, Topology, copy_topology_with_updates from toop_engine_interfaces.folder_structure import OUTPUT_FILE_NAMES, POSTPROCESSING_PATHS, PREPROCESSING_PATHS from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.stored_action_set import ActionSet diff --git a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py index 0564ca267..9e544631a 100644 --- a/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py +++ b/packages/dc_solver_pkg/tests/export/test_station_switch_updates.py @@ -15,7 +15,7 @@ get_changing_switches_from_changed_stations, ) from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import get_changing_switches_from_topology -from toop_engine_interfaces.asset_topology import RawStation, Topology, copy_topology_with_updates +from toop_engine_interfaces.asset_topology.asset_topology import RawStation, Topology, copy_topology_with_updates from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema 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 59eec9508..7d52adae1 100644 --- a/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py +++ b/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py @@ -56,7 +56,7 @@ logger, preprocess_bb_outages, ) -from toop_engine_interfaces.asset_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation # FIxme: Look deeply into this diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py index 234beb4cd..6c0b8e4c7 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_pandapower.py @@ -10,7 +10,7 @@ import pandapower as pp from toop_engine_dc_solver.postprocess.apply_asset_topo_pandapower import apply_station, apply_topology from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import SEPARATOR -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py index 9a8085f3f..a42abd6ea 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_apply_asset_topo_powsybl.py @@ -22,7 +22,7 @@ find_injection, ) from toop_engine_grid_helpers.powsybl.example_grids import basic_node_breaker_network_powsybl -from toop_engine_interfaces.asset_topology import ( +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, Topology, copy_topology_with_updates, diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py b/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py index 7e4dc7b15..b2ff6dcba 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_loadflows_match.py @@ -44,7 +44,7 @@ get_pandapower_loadflow_results_injection, ) from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import table_id -from toop_engine_grid_helpers.powsybl.loadflow_parameters import DISTRIBUTED_SLACK +from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK from toop_engine_interfaces.folder_structure import ( CHRONICS_FILE_NAMES, PREPROCESSING_PATHS, @@ -160,13 +160,13 @@ def test_powsybl_complex_grid_actions_match_runner( _, static_information, network_data = load_grid( data_folder_dirfs=DirFileSystem(str(complex_grid_battery_hvdc_svc_3w_trafo_linear_1_0_data_folder)), parameters=PreprocessParameters(), - lf_params=DISTRIBUTED_SLACK, + lf_params=CGMES_DISTRIBUTED_SLACK, ) action_set = extract_action_set(network_data) nminus1_definition = extract_nminus1_definition(network_data) - runner = PowsyblRunner(lf_params=DISTRIBUTED_SLACK) + runner = PowsyblRunner(lf_params=CGMES_DISTRIBUTED_SLACK) runner.load_base_grid( complex_grid_battery_hvdc_svc_3w_trafo_linear_1_0_data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"] ) diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py index d6a9b8d27..8c5ef7907 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess.py @@ -69,7 +69,8 @@ get_pandapower_branch_loadflow_results_sequence, ) from toop_engine_grid_helpers.pandapower.pandapower_id_helpers import table_ids -from toop_engine_interfaces.asset_topology import MaterializedStation, copy_topology_with_updates +from toop_engine_interfaces.asset_topology.asset_topology import copy_topology_with_updates +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.messages.preprocess.preprocess_heartbeat import PreprocessStage diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py index 32518b1e7..b88acaa82 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_bb_outage.py @@ -26,10 +26,8 @@ update_network_data_with_non_rel_bb_outages, ) from toop_engine_dc_solver.preprocess.preprocess_station_realisations import enumerate_station_realisations -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - InjectionAsset, +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, InjectionAsset +from toop_engine_interfaces.asset_topology.materialized_topology import ( MaterializedAssetConnection, MaterializedStation, ) diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py index c8265338c..05108a485 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py @@ -22,15 +22,9 @@ make_separation_set, prepare_for_separation_set, ) -from toop_engine_interfaces.asset_topology import ( - AssetBranchType, - AssetInjectionType, - Busbar, - BusbarCoupler, - MaterializedAssetConnection, - MaterializedStation, - SwitchableAsset, -) +from toop_engine_interfaces.asset_topology.asset_types import AssetBranchType, AssetInjectionType +from toop_engine_interfaces.asset_topology.assets import Busbar, BusbarCoupler, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation def _combined_asset_connections(station: MaterializedStation) -> list[MaterializedAssetConnection]: diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py b/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py index a6b978eed..21cb7d392 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_realize_assignment.py @@ -16,12 +16,10 @@ from toop_engine_dc_solver.preprocess.preprocess_switching import ( make_optimal_separation_set, ) -from toop_engine_interfaces.asset_topology import ( - Busbar, - BusbarCoupler, +from toop_engine_interfaces.asset_topology.assets import Busbar, BusbarCoupler, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import ( MaterializedAssetConnection, MaterializedStation, - SwitchableAsset, ) diff --git a/packages/dc_solver_pkg/tests/test_example_grids.py b/packages/dc_solver_pkg/tests/test_example_grids.py index 10e258617..11d557e5e 100644 --- a/packages/dc_solver_pkg/tests/test_example_grids.py +++ b/packages/dc_solver_pkg/tests/test_example_grids.py @@ -46,7 +46,7 @@ ) from toop_engine_grid_helpers.powsybl.loadflow_parameters import CGMES_DISTRIBUTED_SLACK, SINGLE_SLACK from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import assert_station_in_network -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.messages.preprocess.preprocess_commands import PreprocessParameters 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 c723627b7..a2d035ed4 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 @@ -16,8 +16,8 @@ from pypowsybl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_asset_topo import get_raw_stations_and_assets_bus_breaker from toop_engine_grid_helpers.powsybl.powsybl_helpers import load_pandapower_net_for_powsybl -from toop_engine_interfaces.asset_topology import Topology -from toop_engine_interfaces.asset_topology_helpers import save_asset_topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology_helpers import save_asset_topology from toop_engine_interfaces.folder_structure import NETWORK_MASK_NAMES, PREPROCESSING_PATHS diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index 2a5a37a7f..f2cda1de3 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -20,18 +20,20 @@ from jaxtyping import Bool from pypowsybl.network.impl.network import Network from toop_engine_grid_helpers.powsybl.powsybl_helpers import change_dangling_to_tie, get_voltage_level_with_region -from toop_engine_interfaces.asset_topology import ( +from toop_engine_interfaces.asset_topology.asset_topology import ( + RawStation, + Topology, +) +from toop_engine_interfaces.asset_topology.assets import ( BranchAsset, Busbar, BusbarCoupler, InjectionAsset, - MaterializedStation, - RawStation, - StationAssetConnection, SwitchableAsset, - Topology, normalize_switchable_asset_payload, ) +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection SwitchableAssetType = TypeVar("SwitchableAssetType", bound=SwitchableAsset) diff --git a/packages/grid_helpers_pkg/tests/conftest.py b/packages/grid_helpers_pkg/tests/conftest.py index df20da0e7..34aab2fe4 100644 --- a/packages/grid_helpers_pkg/tests/conftest.py +++ b/packages/grid_helpers_pkg/tests/conftest.py @@ -15,7 +15,7 @@ from pandapower import pp_dir from pandapower.converter import to_mpc from toop_engine_grid_helpers.powsybl.example_grids import case14_matching_asset_topo_powsybl -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS diff --git a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py index 4c95057a7..10404c94a 100644 --- a/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/tests/powsybl/test_powsybl_asset_topo.py @@ -29,14 +29,11 @@ get_relevant_stations, get_topology, ) -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - BusbarCoupler, - InjectionAsset, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, Topology, ) +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, InjectionAsset from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS diff --git a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py index 1e417abcf..3ab3caf5d 100644 --- a/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py +++ b/packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py @@ -22,7 +22,9 @@ import structlog from beartype.typing import Optional, Union from toop_engine_importer.ucte_toolset.ucte_io import make_ucte, parse_ucte -from toop_engine_interfaces.asset_topology import BusbarCoupler, MaterializedStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.assets import BusbarCoupler +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation logger = structlog.get_logger(__name__) 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 5c4d57cb9..65fc08048 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 @@ -24,7 +24,7 @@ SwitchableAssetSchema, SwitchSchema, ) -from toop_engine_interfaces.asset_topology import ( +from toop_engine_interfaces.asset_topology.assets import ( AssetBay, build_asset_bay_id, ) diff --git a/packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_helper_functions.py b/packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_helper_functions.py index 599f2d6c8..77d9b2e8e 100644 --- a/packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_helper_functions.py +++ b/packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_helper_functions.py @@ -10,7 +10,7 @@ import pandas as pd from beartype.typing import Literal, TypeVar, get_args from toop_engine_importer.network_graph.data_classes import DUPLICATED_EDGE_SUFFIX -from toop_engine_interfaces.asset_topology import SwitchableAsset +from toop_engine_interfaces.asset_topology.assets import SwitchableAsset T = TypeVar("T") K = TypeVar("K") 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 3080a6311..5b71bf43f 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 @@ -51,14 +51,12 @@ get_voltage_level_with_region, ) from toop_engine_importer.pypowsybl_import.powsybl_masks import NetworkMasks -from toop_engine_interfaces.asset_topology import ( - AssetBay, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, - normalize_switchable_asset_payload, - topology_parts_from_materialized_station, ) +from toop_engine_interfaces.asset_topology.assets import AssetBay, normalize_switchable_asset_payload +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.topology_conversion import topology_parts_from_materialized_station from toop_engine_interfaces.messages.preprocess.preprocess_commands import CgmesImporterParameters logger = structlog.get_logger(__name__) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index 484b22d3d..64f470509 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -30,15 +30,12 @@ get_closed_switch, get_indirect_connected_switch, ) -from toop_engine_interfaces.asset_topology import ( - AssetBay, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, - build_asset_bay_id, - normalize_switchable_asset_payload, - topology_parts_from_materialized_station, ) +from toop_engine_interfaces.asset_topology.assets import AssetBay, build_asset_bay_id, normalize_switchable_asset_payload +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.topology_conversion import topology_parts_from_materialized_station logger = structlog.get_logger(__name__) diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py index 06f47864f..dbadbb60d 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py @@ -53,7 +53,7 @@ fuse_closed_switches_by_bus_ids, get_coupler_types_of_substation, ) -from toop_engine_interfaces.asset_topology import Topology, copy_topology_with_updates +from toop_engine_interfaces.asset_topology.asset_topology import Topology, copy_topology_with_updates logger = structlog.get_logger(__name__) 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..f006552e3 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 @@ -10,7 +10,7 @@ from beartype.typing import Optional from pypowsybl.network.impl.network import Network from toop_engine_importer.pypowsybl_import.cgmes.cgmes_toolset import get_voltage_level_with_region -from toop_engine_interfaces.asset_topology import AssetBranchTypePowsybl, AssetInjectionTypePowsybl +from toop_engine_interfaces.asset_topology.asset_types import AssetBranchTypePowsybl, AssetInjectionTypePowsybl from toop_engine_interfaces.messages.preprocess.preprocess_commands import ( RegionType, RelevantStationRules, 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 a90ed2bc0..48e1e4271 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 @@ -48,7 +48,7 @@ create_new_border_limits, ) from toop_engine_importer.pypowsybl_import.powsybl_masks import NetworkMasks, make_masks, save_masks_to_filesystem -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.filesystem_helper import copy_file_fs, save_pydantic_model_fs from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.messages.preprocess.preprocess_commands import ( @@ -420,18 +420,8 @@ def convert_file( ) # get N-1 masks status_update_fn("get_masks", "Creating Network Masks") - slack_id = network.get_extension("slackTerminal").iloc[0].bus_id - network_masks = get_network_masks(network, slack_id, importer_parameters, statistics, filesystem=unprocessed_gridfile_fs) - save_masks_to_filesystem( - data_folder=importer_parameters.data_folder, network_masks=network_masks, filesystem=processed_gridfile_fs - ) - - # get nminus1 definition - nminus1_definition = create_nminus1_definition_from_masks(network, network_masks) - save_pydantic_model_fs( - filesystem=processed_gridfile_fs, - file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"], - pydantic_model=nminus1_definition, + network_masks = compute_network_masks_and_n_1_definition( + importer_parameters, processed_gridfile_fs, unprocessed_gridfile_fs, network, statistics ) if ( @@ -482,6 +472,56 @@ def convert_file( return statistics.import_result +def compute_network_masks_and_n_1_definition( + importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters], + processed_gridfile_fs: AbstractFileSystem, + unprocessed_gridfile_fs: AbstractFileSystem, + network: Network, + statistics: PreProcessingStatistics, +) -> NetworkMasks: + """Create, persist, and return network masks plus the derived N-1 definition. + + Parameters + ---------- + importer_parameters : Union[UcteImporterParameters, CgmesImporterParameters] + Import configuration providing the data folder and mask generation settings. + processed_gridfile_fs : AbstractFileSystem + Filesystem used to persist the generated masks and N-1 definition. + unprocessed_gridfile_fs : AbstractFileSystem + Filesystem used to resolve auxiliary inputs required during mask creation. + network : Network + Powsybl network for which masks and contingencies are computed. + statistics : PreProcessingStatistics + Statistics object updated while generating masks. + + Returns + ------- + NetworkMasks + Generated network masks after saving them and the derived N-1 definition. + """ + slack_id = network.get_extension("slackTerminal").iloc[0].bus_id + network_masks = get_network_masks( + network, + slack_id, + importer_parameters, + statistics, + filesystem=unprocessed_gridfile_fs, + ) + save_masks_to_filesystem( + data_folder=importer_parameters.data_folder, network_masks=network_masks, filesystem=processed_gridfile_fs + ) + + # get nminus1 definition + nminus1_definition = create_nminus1_definition_from_masks(network, network_masks) + save_pydantic_model_fs( + filesystem=processed_gridfile_fs, + file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"], + pydantic_model=nminus1_definition, + ) + + return network_masks + + def get_slack_ids(network: Network) -> list[str] | None: """Get the slack bus ids from the network. diff --git a/packages/importer_pkg/tests/conftest.py b/packages/importer_pkg/tests/conftest.py index 931355c7d..bcf3da42d 100644 --- a/packages/importer_pkg/tests/conftest.py +++ b/packages/importer_pkg/tests/conftest.py @@ -43,7 +43,7 @@ ) from toop_engine_importer.pandapower_import import add_substation_column_to_bus from toop_engine_importer.pypowsybl_import import powsybl_masks, preprocessing -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.messages.preprocess.preprocess_commands import ( AreaSettings, diff --git a/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py b/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py index b5ae7ae48..bd34a1964 100644 --- a/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py +++ b/packages/importer_pkg/tests/exporter/test_asset_topology_to_ucte.py @@ -25,7 +25,7 @@ ) from toop_engine_importer.exporter.uct_exporter import validate_ucte_changes from toop_engine_importer.ucte_toolset.ucte_io import make_ucte, parse_ucte -from toop_engine_interfaces.asset_topology import ( +from toop_engine_interfaces.asset_topology.assets import ( AssetSetpoint, Busbar, BusbarCoupler, diff --git a/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py b/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py index c146997c9..e91b555e5 100644 --- a/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py +++ b/packages/importer_pkg/tests/network_graph/test_graph_to_asset_topo.py @@ -31,7 +31,7 @@ ) from toop_engine_importer.network_graph.network_graph_data import add_graph_specific_data from toop_engine_importer.network_graph.powsybl_station_to_graph import get_station -from toop_engine_interfaces.asset_topology import AssetBay, build_asset_bay_id +from toop_engine_interfaces.asset_topology.assets import AssetBay, build_asset_bay_id def test_remove_double_connections(): diff --git a/packages/importer_pkg/tests/network_graph/test_network_graph_helper_functions.py b/packages/importer_pkg/tests/network_graph/test_network_graph_helper_functions.py index f81ed6e71..f4953c417 100644 --- a/packages/importer_pkg/tests/network_graph/test_network_graph_helper_functions.py +++ b/packages/importer_pkg/tests/network_graph/test_network_graph_helper_functions.py @@ -17,7 +17,7 @@ remove_suffix_from_switchable_assets, reverse_dict_list, ) -from toop_engine_interfaces.asset_topology import SwitchableAsset +from toop_engine_interfaces.asset_topology.assets import SwitchableAsset def test_find_busbar_in_list(): 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 ccd84dabe..2d1d1bafc 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 @@ -38,7 +38,8 @@ ) from toop_engine_importer.pypowsybl_import import powsybl_masks from toop_engine_importer.pypowsybl_import.cgmes.cgmes_toolset import get_busbar_sections_with_in_service -from toop_engine_interfaces.asset_topology import MaterializedStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.messages.preprocess.preprocess_commands import ( AreaSettings, CgmesImporterParameters, diff --git a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py index 055a09a15..fee9e8169 100644 --- a/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py +++ b/packages/importer_pkg/tests/pandapower/test_asset_topology_pandapower.py @@ -12,12 +12,11 @@ import pytest import structlog.testing from toop_engine_importer.pandapower_import import asset_topology -from toop_engine_interfaces.asset_topology import ( - AssetBay, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, - build_asset_bay_id, ) +from toop_engine_interfaces.asset_topology.assets import AssetBay, build_asset_bay_id +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation def test_get_busses_from_station(pp_network_w_switches): diff --git a/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py b/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py index b0bb1be44..07c8abbb8 100644 --- a/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py +++ b/packages/importer_pkg/tests/pandapower/test_preprocessing_pandapower.py @@ -18,7 +18,7 @@ pandapower_toolset_node_breaker, preprocessing, ) -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology def test_handle_switches(pp_network_w_switches): diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index dd500d8f1..5e43caf0c 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -25,13 +25,12 @@ from toop_engine_importer.pypowsybl_import import powsybl_masks, preprocessing from toop_engine_importer.pypowsybl_import.data_classes import PreProcessingStatistics from toop_engine_importer.pypowsybl_import.preprocessing import create_nminus1_definition_from_masks -from toop_engine_interfaces.asset_topology import ( - Busbar, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - StationAssetConnection, - SwitchableAsset, Topology, ) +from toop_engine_interfaces.asset_topology.assets import Busbar, SwitchableAsset +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection from toop_engine_interfaces.folder_structure import ( NETWORK_MASK_NAMES, PREPROCESSING_PATHS, diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py deleted file mode 100644 index d68d06580..000000000 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology.py +++ /dev/null @@ -1,1441 +0,0 @@ -# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV -# -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, -# you can obtain one at https://mozilla.org/MPL/2.0/. -# Mozilla Public License, version 2.0 - -"""Contains the data models for the asset topology.""" - -from copy import deepcopy -from datetime import datetime -from enum import Enum - -import numpy as np -from beartype.typing import Any, Collection, Literal, Optional, TypeAlias, Union, get_args -from numpydantic import NDArray, Shape -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator - -StationSwitchingArray: TypeAlias = NDArray[Shape["* n_bus, * n_asset"], np.bool_] - - -def _merged_round_trip_payload(model: BaseModel, update: Optional[dict[str, Any]], *, deep: bool = False) -> dict[str, Any]: - """Merge model field values and requested updates for revalidation-aware model_copy overrides.""" - payload = {field_name: getattr(model, field_name) for field_name in type(model).model_fields} - if deep: - payload = deepcopy(payload) - if update: - payload.update(update) - return payload - - -class PowsyblSwitchValues(Enum): - """Enum for the switch values in the Powsybl model.""" - - OPEN = True - """ The switch is open, i.e. not connected.""" - CLOSED = False - """ The switch is closed, i.e. connected.""" - - -BranchEnd: TypeAlias = Literal["from", "to", "hv", "mv", "lv"] -AssetBranchTypePandapower: TypeAlias = Literal[ - "line", - "trafo", - "trafo3w_lv", - "trafo3w_mv", - "trafo3w_hv", - "impedance", - "xward", -] -AssetBranchTypePowsybl: TypeAlias = Literal[ - "LINE", - "TWO_WINDINGS_TRANSFORMER", - "TIE_LINE", -] -AssetBranchType: TypeAlias = Literal[AssetBranchTypePandapower, AssetBranchTypePowsybl] - -AssetInjectionTypePandapower: TypeAlias = Literal[ - "ext_grid", - "gen", - "load", - "shunt", - "sgen", - "ward", - "ward_load", - "ward_shunt", - "xward_load", - "xward_shunt", - "dcline_from", - "dcline_to", -] -AssetInjectionTypePowsybl: TypeAlias = Literal[ - "LOAD", - "GENERATOR", - "BOUNDARY_LINE", - "HVDC_CONVERTER_STATION", - "STATIC_VAR_COMPENSATOR", - "SHUNT_COMPENSATOR", - "BATTERY", -] -AssetInjectionType: TypeAlias = Literal[AssetInjectionTypePandapower, AssetInjectionTypePowsybl] -AssetType: TypeAlias = Literal[AssetBranchType, AssetInjectionType] - - -class Busbar(BaseModel): - """Busbar data describing a single busbar a station.""" - - grid_model_id: str - """ The unique identifier of the busbar. - Corresponds to the busbar's id in the grid model.""" - - busbar_type: Optional[str] = None - """ The type of the busbar, might be useful for finding the busbar later on """ - - name: Optional[str] = None - """ The name of the busbar, might be useful for finding the busbar later on """ - - int_id: int - """ Is used to reference busbars in the couplers. Needs to be unique per station""" - - in_service: bool = True - """ Whether the busbar is in service. If False, it will be ignored in the switching table""" - - bus_branch_bus_id: Optional[str] = None - """ The bus_branch_bus_id refers to the bus-branch model bus id. - There might be a difference between the busbar grid_model_id (a physical busbar) - and the bus_branch_bus_id from the bus-branch model. - Use this bus_branch_bus_id to store the bus-branch model bus id. - Note: the Station grid_model_id also a bus-branch bus_branch_bus_id. This id is the most splitable bus_branch_bus_id. - Other bus_branch_bus_ids are part of the physical station, but are separated by a coupler or branch.""" - - -class AssetBay(BaseModel): - """Saves the physical connection from the asset to the substation busbars - a bay (Schaltfeld). - - A line usually has three switches, before it is connected to the busbar. - Two disconnector switches and one circuit breaker switch. - A transformer usually has two switches, before it is connected to the busbar. - One disconnector switch and one circuit breaker switch. - - type: n - node - type: b - busbar (Sammelschiene) - type: CB - DV Circuit Breaker / Power Switch (Leistungsschalter) - type: DS - Disconnector Switch (Trennschalter) - - ------------------ busbar 1 - type: b - | - / type: DS - SR Switch busbar 1 -> used for reassigning the asset to another busbar - | - ------|----------- busbar 2 - type: b - | | - / | type: DS - SR Switch busbar 2 -> used for reassigning the asset to another busbar - | | - --------- bus_3 - type: n - busbar section bus - | - / type: CB - DV Circuit Breaker / Power Switch -> used for disconnecting the asset from the busbar - | - --------- bus_2 - type: n - circuit breaker bus - | - / type: DS - SL Switch (optional) -> not used by the asset - | - --------- bus_1 - type: n - asset bus - ^ - | Line/Transformer - - - """ - - asset_bay_id: str - """Topology-scoped identifier for the asset bay.""" - - sl_switch_grid_model_id: Optional[str] = None - """ The id of the switch, which connects the asset to the circuit breaker node. - This switch is a disconnector switch. Do not use for anything, leave state as found. - Default should be closed.""" - - dv_switch_grid_model_id: str - """ This switch is a circuit breaker / power switch. - Use for disconnecting / reconnecting the asset from the busbar. """ - - sr_switch_grid_model_id: dict[str, str] - """ The ids of the switches, which assign the asset to the busbars. - key: busbar_grid_model_id e.g. 4%%bus - value: sr_switch_grid_model_id - This switch is a disconnector switch. Use for reassigning the asset to another busbar. - Only one switch should be closed at a time. - """ - - @field_validator("sr_switch_grid_model_id") - @classmethod - def check_is_empty(cls, v: dict[str, str]) -> dict[str, str]: - """Check if the dict is empty. - - Parameters - ---------- - v : dict[str, str] - The dictionary of sr_switch_grid_model_id to check. - - Returns - ------- - dict[str, str] - The dictionary itself. - - Raises - ------ - ValueError - If the dictionary is empty. - """ - if len(v) == 0: - raise ValueError("sr_switch_grid_model_id must not be empty") - return v - - -class BusbarCoupler(BaseModel): - """Coupler data describing a single coupler at a station. - - This references only busbar couplers, i.e. couplers connecting two busbars. - Switches connecting assets to a busbar are represented in the asset_switching_table in the station model. - - Note: A busbar couple is a physical connection between two busbars, this can be also a - cross coupler. To further specify the connection of an asset to a busbar, the asset connection - """ - - grid_model_id: str - """ The unique identifier of the coupler. - Corresponds to the coupler's id in the grid model.""" - - coupler_type: Optional[str] = None - """ The type of the coupler, might be useful for finding the coupler later on """ - - name: Optional[str] = None - """ The name of the coupler, might be useful for finding the coupler later on """ - - # TODO: this does not work for a coupler with multiple busbars on one side - busbar_from_id: int - """ Is used to determine where the coupler is connected to the busbars on the "from" side. - Refers to the int_id of the busbar""" - - # TODO: this does not work for a coupler with multiple busbars on one side - busbar_to_id: int - """ Is used to determine where the coupler is connected to the busbars on the "to" side. - Refers to the int_id of the busbar""" - - open: bool - """ The status of the coupler. True if the coupler is open, False if the coupler is closed. - TODO: Switch to using the connectivity table instead of this field. - """ - - in_service: bool = True - """ Whether the coupler is in-service. Out-of-service couplers are assumed to be always open""" - - asset_bay: Optional[AssetBay] = None - """ The asset bay (Schaltfeld) of the coupler. - Note: A coupler can have multiple from and to busbars. - The asset bay sr_switch_grid_model_id is used save the selector switches of the coupler. - Note: A coupler has never a sl_switch_grid_model_id, the dv_switch_grid_model_id should - the same as the name of the coupler. - - """ - - -class SwitchableAsset(BaseModel): - """Asset data describing a single asset at a station. - - An asset can be for instance a transformer, line, generator, load, shunt. - Note: An asset can be connected to multiple busbars through the switching grid, however if - this happens a closed coupler between these busbars is assumed. If such couplers are not present, - they will be created. - Note: An asset that is out-of-service can be represented, but its switching entries will be - ignored. - """ - - grid_model_id: str - """ The unique identifier of the asset. - Corresponds to the asset's id in the grid model.""" - - asset_type: Optional[AssetType] = None - """ The type of the asset. These refer loosely to the types in the pandapower/powsybl grid - models. If set, this can be used to disambiguate branches from injections """ - - name: Optional[str] = None - """ The name of the asset, might be useful for finding the asset later on """ - - in_service: bool = True - """ If the element is in service. False means the switching entry for this element will be - ignored. This shall not be used for elements intentionally disconnected, instead set all zeros - in the switching table.""" - - def is_branch(self) -> Optional[bool]: - """Return True if the asset is a branch. - - Only works if the type is set. If type is not set this will return None. - - Returns - ------- - bool - True if the asset is a branch, False if it is an injection. - """ - if self.asset_type is None: - return None - return self.asset_type in get_args(AssetBranchType) - - -class BranchAsset(SwitchableAsset): - """Switchable asset representing a branch-type element.""" - - asset_type: Optional[AssetBranchType] = None - - def is_branch(self) -> Optional[bool]: - """Return branch semantics for branch assets.""" - return None if self.asset_type is None else True - - -class InjectionAsset(SwitchableAsset): - """Switchable asset representing an injection-type element.""" - - asset_type: Optional[AssetInjectionType] = None - - def is_branch(self) -> Optional[bool]: - """Return branch semantics for injection assets.""" - return None if self.asset_type is None else False - - -def normalize_switchable_asset_payload(asset: dict[str, Any]) -> SwitchableAsset: - """Normalize an asset payload to the matching branch or injection subclass when possible.""" - if isinstance(asset, (BranchAsset, InjectionAsset)): - return asset - - asset_data = asset.model_dump() if isinstance(asset, SwitchableAsset) else dict(asset) - if "asset_type" not in asset_data and "type" in asset_data: - asset_data["asset_type"] = asset_data.pop("type") - asset_type = asset_data.get("asset_type") - if asset_type in get_args(AssetBranchType): - return BranchAsset(**asset_data) - if asset_type in get_args(AssetInjectionType): - return InjectionAsset(**asset_data) - return SwitchableAsset(**asset_data) - - -class StationAssetConnection(BaseModel): - """Station-local association between a switching-table column and a topology asset.""" - - asset_id: str - """Grid model id of the topology-owned asset referenced by this station-local column.""" - - terminal: Optional[BranchEnd] = None - """Optional branch terminal metadata for this station-local asset occurrence.""" - - asset_bay_id: Optional[str] = None - """Optional topology-scoped asset bay identifier for this station-local asset occurrence.""" - - -class MaterializedAssetConnection(BaseModel): - """Station-local association between a switching-table column and a materialized asset payload.""" - - asset: SwitchableAsset - """Station-local asset payload aligned with one switching-table column.""" - - terminal: Optional[BranchEnd] = None - """Optional branch terminal metadata for this station-local asset occurrence.""" - - asset_bay: Optional[AssetBay] = None - """Optional station-local asset bay payload for this station-local asset occurrence.""" - - def get_sr_switch(self) -> Optional[dict[str, str]]: - """Return the sr_switch_grid_model_id dict from the asset bay if it exists.""" - if self.asset_bay is not None: - return self.asset_bay.sr_switch_grid_model_id - return None - - -class AssetSetpoint(BaseModel): - """Asset data describing a single asset with a setpoint. - - This could for example be a PST or HVDC setpoint. - Note: The same asset can both be switchable and have a setpoint. In this case, the asset will - be represented twice. - """ - - grid_model_id: str - """ The unique identifier of the asset. - Corresponds to the asset's id in the grid model.""" - - asset_type: Optional[str] = None - """ The type of the asset, might be useful for finding the asset later on """ - - name: Optional[str] = None - """ The name of the asset, might be useful for finding the asset later on """ - - setpoint: float - """ The setpoint of the asset. """ - - -def _validate_station_switching_tables( - station_grid_model_id: str, - station_name: Optional[str], - busbar_count: int, - asset_count: int, - asset_switching_table: np.ndarray, - asset_connectivity: Optional[np.ndarray], - asset_kind: str, -) -> None: - """Validate switching-table shapes against the station dimensions. - - Parameters - ---------- - station_grid_model_id : str - Grid model id of the station being validated. - station_name : Optional[str] - Human-readable station name used in validation errors. - busbar_count : int - Expected number of busbar rows in the switching tables. - asset_count : int - Expected number of asset columns in the switching tables. - asset_switching_table : np.ndarray - Current station switching table. - asset_connectivity : Optional[np.ndarray] - Optional connectivity mask describing physically allowed assignments. - asset_kind: str - The kind of asset being validated, used in error messages (e.g. "branch" or "injection"). - - Returns - ------- - None - This function returns nothing and raises on invalid shapes. - - Raises - ------ - ValueError - If either switching table does not match the expected station dimensions. - """ - if asset_switching_table.shape != (busbar_count, asset_count): - raise ValueError( - f"{asset_kind}_switching_table shape {asset_switching_table.shape} does not match busbars " - f"{busbar_count} and {asset_kind} assets {asset_count}" - f" Station_id: {station_grid_model_id}, Name: {station_name}" - ) - - if asset_connectivity is not None and asset_connectivity.shape != (busbar_count, asset_count): - raise ValueError( - f"{asset_kind}_connectivity shape {asset_connectivity.shape} does not match busbars " - f"{busbar_count} and {asset_kind} assets {asset_count}" - f" Station_id: {station_grid_model_id}, Name: {station_name}" - ) - - -def _validate_station_physical_assignments( - station_grid_model_id: str, - station_name: Optional[str], - asset_switching_table: np.ndarray, - asset_connectivity: Optional[np.ndarray], - asset_kind: str, -) -> None: - """Validate that all current assignments are physically allowed. - - Parameters - ---------- - station_grid_model_id : str - Grid model id of the station being validated. - station_name : Optional[str] - Human-readable station name used in validation errors. - asset_switching_table : np.ndarray - Current station switching table. - asset_connectivity : Optional[np.ndarray] - Optional connectivity mask describing physically allowed assignments. - asset_kind: str - The kind of asset being validated, used in error messages (e.g. "branch" or "injection"). - - Returns - ------- - None - This function returns nothing and raises on invalid assignments. - - Raises - ------ - ValueError - If the switching table contains assignments forbidden by ``asset_connectivity``. - """ - if asset_connectivity is not None: - if np.logical_and(asset_switching_table, np.logical_not(asset_connectivity)).any(): - raise ValueError( - f"Not all current {asset_kind} assignments are physically allowed " - f"Station_id: {station_grid_model_id}, Name: {station_name}" - ) - - -class _StationStructure(BaseModel): - """Shared station fields and structural validators for station views.""" - - model_config = ConfigDict(arbitrary_types_allowed=True) - - grid_model_id: str - """The unique identifier of the station. - - Expects the bus-branch model bus_id, not the full voltage level id. - - Included are all assets, busbars and couplers that are connectable via switches. - Buses in the same station that are connected via branches are excluded in this specific bus. - - This means, that two stations/buses can have the same elements if the station is currently split. - """ - - name: Optional[str] = None - """The name of the station.""" - - station_type: Optional[str] = None - """The type of the station.""" - - region: Optional[str] = None - """The region of the station.""" - - voltage_level: Optional[float] = None - """The voltage level of the station.""" - - busbars: list[Busbar] - """The list of busbars at the station.""" - - couplers: list[BusbarCoupler] - """The list of couplers at the station.""" - - branch_switching_table: StationSwitchingArray - """Holds the switching of each branch asset to each busbar, shape (n_bus, n_branch_asset). - - An entry is true if the asset is connected to the busbar. - Note: An asset can be connected to multiple busbars, in which case a closed coupler is assumed - to be present between these busbars. - Note: An asset can be connected to none of the busbars. In this case, the asset is intentionally - disconnected as part of a transmission line switching action. In practice, this usually involves - a separate switch from the asset-to-busbar couplers, as each asset usually has a switch that - completely disconnects it from the station. These switches are not modelled here, a - postprocessing routine needs to do the translation to this physical layout. Do not use - in_service for intentional disconnections. - """ - - injection_switching_table: StationSwitchingArray - """Holds the switching of each injection asset to each busbar, shape (n_bus, n_injection_asset).""" - - branch_connectivity: Optional[StationSwitchingArray] = None - """Holds all physically possible branch layouts, shape (n_bus, n_branch_asset).""" - - injection_connectivity: Optional[StationSwitchingArray] = None - """Holds all physically possible injection layouts, shape (n_bus, n_injection_asset).""" - - model_log: Optional[list[str]] = None - """Holds log messages from the model creation process. - - This can be used to store information about the model creation process, e.g. warnings or errors. - A potential use case is to inform the user about data quality issues e.g. missing the Asset Bay switches. - """ - - @field_validator( - "branch_switching_table", - "injection_switching_table", - "branch_connectivity", - "injection_connectivity", - mode="before", - ) - @classmethod - def normalize_station_tables(cls, v: Optional[Collection]) -> Optional[np.ndarray]: - """Normalize split station tables to boolean numpy arrays.""" - if v is None: - return None - return np.asarray(v, dtype=bool) - - @field_validator("busbars") - @classmethod - def check_int_id_unique(cls, v: list[Busbar]) -> list[Busbar]: - """Check if int_id is unique for all busbars.""" - int_ids = [busbar.int_id for busbar in v] - if len(int_ids) != len(set(int_ids)): - raise ValueError("int_id must be unique for busbars") - return v - - @field_validator("couplers") - @classmethod - def check_coupler_busbars_different(cls, v: list[BusbarCoupler]) -> list[BusbarCoupler]: - """Check if busbar_from_id and busbar_to_id are different for all couplers.""" - for coupler in v: - if coupler.busbar_from_id == coupler.busbar_to_id: - raise ValueError(f"busbar_from_id and busbar_to_id must be different for coupler {coupler.grid_model_id}") - return v - - @model_validator(mode="after") - def check_busbar_exists(self: "_StationStructure") -> "_StationStructure": - """Check if all busbars in couplers exist in the busbars list.""" - busbar_ids = [busbar.int_id for busbar in self.busbars] - for coupler in self.couplers: - if coupler.busbar_from_id not in busbar_ids: - raise ValueError( - f"busbar_from_id {coupler.busbar_from_id} in coupler {coupler.grid_model_id} does not exist in busbars." - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - if coupler.busbar_to_id not in busbar_ids: - raise ValueError( - f"busbar_to_id {coupler.busbar_to_id} in coupler {coupler.grid_model_id} does not exist in busbars" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - return self - - @model_validator(mode="after") - def check_coupler_references(self: "_StationStructure") -> "_StationStructure": - """Check if all closed couplers reference in-service busbars.""" - busbar_state_map = {busbar.int_id: busbar.in_service for busbar in self.busbars} - for coupler in self.couplers: - if coupler.open or not coupler.in_service: - continue - if busbar_state_map[coupler.busbar_from_id] != busbar_state_map[coupler.busbar_to_id]: - raise ValueError( - f"Closed coupler {coupler.grid_model_id} connects out-of-service busbar with in-service busbar." - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - return self - - @model_validator(mode="after") - def check_bus_id(self: "_StationStructure") -> "_StationStructure": - """Check if station grid_model_id is in the busbar.bus_branch_bus_id.""" - busbar_grid_model_id = [busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id is not None] - if len(busbar_grid_model_id) > 0 and self.grid_model_id not in busbar_grid_model_id: - raise ValueError( - f"Station grid_model_id {self.grid_model_id} does not exist in busbars bus_branch_bus_id" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - return self - - def is_split(self) -> bool: - """Return whether the station view spans more than one non-empty bus-branch bus id.""" - bus_ids = {busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id not in {None, ""}} - return len(bus_ids) > 1 - - -class MaterializedStation(_StationStructure): - """Station data describing a single materialized station. - - The station identity refers to a single bus-branch model bus_id that represents one splitable - station view. - A physical substation or voltage level may contain multiple bus-branch model bus_ids. - The station assets are aligned with the switching tables and describe the assets visible in that - station view; they are not intended to define a topology-owned canonical asset list. - """ - - branch_connections: list[MaterializedAssetConnection] = Field(default_factory=list) - """Station-local branch payloads aligned with ``branch_switching_table``.""" - - injection_connections: list[MaterializedAssetConnection] = Field(default_factory=list) - """Station-local injection payloads aligned with ``injection_switching_table``.""" - - @model_validator(mode="after") - def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": - """Check if switching-table-aligned station-local assets match the matrix shapes.""" - _validate_station_switching_tables( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - busbar_count=len(self.busbars), - asset_count=len(self.branch_connections), - asset_switching_table=self.branch_switching_table, - asset_connectivity=self.branch_connectivity, - asset_kind="branch", - ) - _validate_station_physical_assignments( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - asset_switching_table=self.branch_switching_table, - asset_connectivity=self.branch_connectivity, - asset_kind="branch", - ) - _validate_station_switching_tables( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - busbar_count=len(self.busbars), - asset_count=len(self.injection_connections), - asset_switching_table=self.injection_switching_table, - asset_connectivity=self.injection_connectivity, - asset_kind="injection", - ) - _validate_station_physical_assignments( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - asset_switching_table=self.injection_switching_table, - asset_connectivity=self.injection_connectivity, - asset_kind="injection", - ) - return self - - @model_validator(mode="after") - def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": - """Check if the asset bay bus is in busbars.""" - busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] - for asset_connection in [*self.branch_connections, *self.injection_connections]: - asset = asset_connection.asset - asset_bay = asset_connection.asset_bay - if asset_bay is not None: - for busbar_id in asset_bay.sr_switch_grid_model_id.keys(): - if busbar_id not in busbar_grid_model_id: - raise ValueError( - f"busbar_id {busbar_id} in asset {asset.grid_model_id} does not exist in busbars" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - return self - - def __eq__(self, other: object) -> bool: - """Check if two stations are equal. - - Parameters - ---------- - other : object - The other station to compare to. - - Returns - ------- - bool - True if the stations are equal, False otherwise. - """ - if not isinstance(other, MaterializedStation): - return False - return ( - self.grid_model_id == other.grid_model_id - and self.region == other.region - and self.busbars == other.busbars - and self.couplers == other.couplers - and self.branch_connections == other.branch_connections - and self.injection_connections == other.injection_connections - and np.array_equal(self.branch_switching_table, other.branch_switching_table) - and np.array_equal(self.injection_switching_table, other.injection_switching_table) - and ( - np.array_equal(self.branch_connectivity, other.branch_connectivity) - if (self.branch_connectivity is not None and other.branch_connectivity is not None) - else self.branch_connectivity == other.branch_connectivity - ) - and ( - np.array_equal(self.injection_connectivity, other.injection_connectivity) - if (self.injection_connectivity is not None and other.injection_connectivity is not None) - else self.injection_connectivity == other.injection_connectivity - ) - ) - - def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "MaterializedStation": - """Copy and revalidate the station.""" - payload = _merged_round_trip_payload(self, update, deep=deep) - return type(self).model_validate(payload) - - def get_connected_assets( - self, - busbar_index: int, - topology_assets: Optional[list[SwitchableAsset]] = None, - asset_scope: Literal["all", "branch", "injection"] = "all", - ) -> list[SwitchableAsset]: - """Return in-service assets connected to one busbar. - - Parameters - ---------- - busbar_index : int - Row index into the station switching tables. - topology_assets : Optional[list[SwitchableAsset]] - Ignored for materialized stations because payloads are embedded locally. - asset_scope : Literal["all", "branch", "injection"] - Restrict the lookup to branch or injection connections. - - Returns - ------- - list[SwitchableAsset] - Connected in-service assets for the requested busbar and scope. - """ - del topology_assets - if asset_scope == "branch": - return [ - asset_connection.asset - for asset_connection, is_connected in zip( - self.branch_connections, - self.branch_switching_table[busbar_index], - strict=True, - ) - if is_connected and asset_connection.asset.in_service - ] - if asset_scope == "injection": - return [ - asset_connection.asset - for asset_connection, is_connected in zip( - self.injection_connections, - self.injection_switching_table[busbar_index], - strict=True, - ) - if is_connected and asset_connection.asset.in_service - ] - return [ - *self.get_connected_assets(busbar_index, asset_scope="branch"), - *self.get_connected_assets(busbar_index, asset_scope="injection"), - ] - - -class RawStation(_StationStructure): - """Station data stored inside a topology without embedded asset payloads. - - The station identity still refers to a bus-branch model bus_id for one splitable station view. - Asset membership is expressed through the aligned station-local arrays instead of embedded - SwitchableAsset payloads. - """ - - branch_connections: list[StationAssetConnection] = Field(default_factory=list) - """Station-local branch references aligned with ``branch_switching_table``.""" - - injection_connections: list[StationAssetConnection] = Field(default_factory=list) - """Station-local injection references aligned with ``injection_switching_table``.""" - - def with_asset_terminals(self, asset_terminals: list[Optional[BranchEnd]]) -> "RawStation": - """Return a copy with updated branch terminals.""" - if len(asset_terminals) != len(self.branch_connections): - raise ValueError( - f"asset_terminals length {len(asset_terminals)} does not match branch_connections length " - f"{len(self.branch_connections)}" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - return self.model_copy( - update={ - "branch_connections": [ - asset_connection.model_copy(update={"terminal": asset_terminal}) - for asset_connection, asset_terminal in zip(self.branch_connections, asset_terminals, strict=True) - ] - } - ) - - @model_validator(mode="after") - def check_asset_reference_alignment(self: "RawStation") -> "RawStation": - """Check if station-local asset reference arrays are aligned.""" - _validate_station_switching_tables( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - busbar_count=len(self.busbars), - asset_count=len(self.branch_connections), - asset_switching_table=self.branch_switching_table, - asset_connectivity=self.branch_connectivity, - asset_kind="branch", - ) - _validate_station_physical_assignments( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - asset_switching_table=self.branch_switching_table, - asset_connectivity=self.branch_connectivity, - asset_kind="branch", - ) - _validate_station_switching_tables( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - busbar_count=len(self.busbars), - asset_count=len(self.injection_connections), - asset_switching_table=self.injection_switching_table, - asset_connectivity=self.injection_connectivity, - asset_kind="injection", - ) - _validate_station_physical_assignments( - station_grid_model_id=self.grid_model_id, - station_name=self.name, - asset_switching_table=self.injection_switching_table, - asset_connectivity=self.injection_connectivity, - asset_kind="injection", - ) - return self - - def __eq__(self, other: object) -> bool: - """Check if two topology stations are equal.""" - if not isinstance(other, RawStation): - return False - return ( - self.grid_model_id == other.grid_model_id - and self.name == other.name - and self.station_type == other.station_type - and self.region == other.region - and self.voltage_level == other.voltage_level - and self.busbars == other.busbars - and self.couplers == other.couplers - and self.branch_connections == other.branch_connections - and self.injection_connections == other.injection_connections - and np.array_equal(self.branch_switching_table, other.branch_switching_table) - and np.array_equal(self.injection_switching_table, other.injection_switching_table) - and ( - np.array_equal(self.branch_connectivity, other.branch_connectivity) - if (self.branch_connectivity is not None and other.branch_connectivity is not None) - else self.branch_connectivity == other.branch_connectivity - ) - and ( - np.array_equal(self.injection_connectivity, other.injection_connectivity) - if (self.injection_connectivity is not None and other.injection_connectivity is not None) - else self.injection_connectivity == other.injection_connectivity - ) - and self.model_log == other.model_log - ) - - def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "RawStation": - """Copy and revalidate the station.""" - payload = _merged_round_trip_payload(self, update, deep=deep) - return type(self).model_validate(payload) - - def get_connected_assets( - self, - busbar_index: int, - topology_assets: Optional[list[SwitchableAsset]] = None, - asset_scope: Literal["all", "branch", "injection"] = "all", - ) -> list[SwitchableAsset]: - """Return in-service topology assets connected to one busbar. - - Parameters - ---------- - busbar_index : int - Row index into the station switching tables. - topology_assets : Optional[list[SwitchableAsset]] - Topology-owned assets used to resolve raw station asset references. - asset_scope : Literal["all", "branch", "injection"] - Restrict the lookup to branch or injection connections. - - Returns - ------- - list[SwitchableAsset] - Connected in-service assets for the requested busbar and scope. - - Raises - ------ - ValueError - If topology assets are missing for a raw station lookup. - """ - if topology_assets is None: - raise ValueError("topology_assets must be provided when resolving connected assets for a RawStation") - - asset_map = {asset.grid_model_id: asset for asset in topology_assets} - if asset_scope == "branch": - return [ - asset_map[asset_connection.asset_id] - for asset_connection, is_connected in zip( - self.branch_connections, - self.branch_switching_table[busbar_index], - strict=True, - ) - if is_connected and asset_map[asset_connection.asset_id].in_service - ] - if asset_scope == "injection": - return [ - asset_map[asset_connection.asset_id] - for asset_connection, is_connected in zip( - self.injection_connections, - self.injection_switching_table[busbar_index], - strict=True, - ) - if is_connected and asset_map[asset_connection.asset_id].in_service - ] - return [ - *self.get_connected_assets(busbar_index, topology_assets=topology_assets, asset_scope="branch"), - *self.get_connected_assets(busbar_index, topology_assets=topology_assets, asset_scope="injection"), - ] - - -class Topology(BaseModel): - """Topology data describing a single timestep topology. - - A topology includes lean station records in raw_stations, topology-owned canonical assets and - asset bays, and potentially asset setpoints. - Use materialize_stations() to reconstruct rich Station objects. - """ - - topology_id: str - """ The unique identifier of the topology. """ - - grid_model_file: Optional[str] = None - """ The grid model file that represents this timestep. Note that relevant folders might only - work on the machine they have been created, so some sort of permanent storage server should be - used to keep these files globally accessible""" - - name: Optional[str] = None - """ The name of the topology. """ - - raw_stations: list[RawStation] - """The topology-owned station records without embedded asset payloads. - - Each raw station represents one bus-branch bus view of a splitable station. - """ - - branch_assets: list[SwitchableAsset] = Field(default_factory=list) - """The topology-owned canonical branch payloads.""" - - injection_assets: list[SwitchableAsset] = Field(default_factory=list) - """The topology-owned canonical injection payloads. - - Station-local branch-end and asset-bay assignment data are stored on raw_stations instead of on - these canonical payloads. - """ - - asset_bays: list[AssetBay] = Field(default_factory=list) - """The topology-owned asset bay payloads.""" - - asset_setpoints: Optional[list[AssetSetpoint]] = None - """ The list of asset setpoints in the topology. """ - - timestamp: datetime - """ The timestamp which is represented by this topology during the original optimization. I.e. - if this timestep was the 5 o clock timestep on the day that was optimized, then this timestamp - would read 5 o clock. """ - - metrics: Optional[dict[str, float]] = None - """ The metrics of the topology. """ - - @field_validator("branch_assets") - @classmethod - def check_branch_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: - """Check if all topology branch assets have unique grid model ids.""" - asset_ids = [asset.grid_model_id for asset in v] - if len(asset_ids) != len(set(asset_ids)): - raise ValueError("grid_model_id must be unique for topology branch assets") - if any(asset.is_branch() is False for asset in v): - raise ValueError("branch_assets must not contain injection assets") - return v - - @field_validator("injection_assets") - @classmethod - def check_injection_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: - """Check if all topology injection assets have unique grid model ids.""" - asset_ids = [asset.grid_model_id for asset in v] - if len(asset_ids) != len(set(asset_ids)): - raise ValueError("grid_model_id must be unique for topology injection assets") - if any(asset.is_branch() is True for asset in v): - raise ValueError("injection_assets must not contain branch assets") - return v - - @field_validator("asset_bays") - @classmethod - def check_asset_bay_ids_unique(cls, v: list[AssetBay]) -> list[AssetBay]: - """Check if all topology asset bay ids are unique.""" - asset_bay_ids = [asset_bay.asset_bay_id for asset_bay in v] - if any(asset_bay_id is None for asset_bay_id in asset_bay_ids): - raise ValueError("All topology asset bays must define asset_bay_id") - if len(asset_bay_ids) != len(set(asset_bay_ids)): - raise ValueError("asset_bay_id must be unique for topology asset bays") - return v - - @model_validator(mode="after") - def check_station_asset_references(self: "Topology") -> "Topology": - """Check if all station asset references exist in the topology-owned collections.""" - branch_asset_ids = {asset.grid_model_id for asset in self.branch_assets} - injection_asset_ids = {asset.grid_model_id for asset in self.injection_assets} - asset_bay_ids = {asset_bay.asset_bay_id for asset_bay in self.asset_bays} - - for station in self.raw_stations: - for asset_connection in station.branch_connections: - asset_id = asset_connection.asset_id - if asset_id not in branch_asset_ids: - raise ValueError( - f"Branch asset grid_model_id {asset_id} referenced by station " - f"{station.grid_model_id} does not exist in topology assets" - ) - for asset_connection in station.injection_connections: - asset_id = asset_connection.asset_id - if asset_id not in injection_asset_ids: - raise ValueError( - f"Injection asset grid_model_id {asset_id} referenced by station " - f"{station.grid_model_id} does not exist in topology assets" - ) - for asset_connection in [*station.branch_connections, *station.injection_connections]: - asset_bay_id = asset_connection.asset_bay_id - if asset_bay_id is not None and asset_bay_id not in asset_bay_ids: - raise ValueError( - f"asset_bay_id {asset_bay_id} referenced by station " - f"{station.grid_model_id} does not exist in topology asset bays" - ) - - return self - - def materialize_stations(self) -> list[MaterializedStation]: - """Materialize station-local asset payloads from topology-owned assets and asset bays.""" - branch_asset_map = {asset.grid_model_id: asset for asset in self.branch_assets} - injection_asset_map = {asset.grid_model_id: asset for asset in self.injection_assets} - asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} - materialized_stations: list[MaterializedStation] = [] - - for station in self.raw_stations: - station_branch_assets = [ - branch_asset_map[asset_connection.asset_id].model_copy(deep=True) - for asset_connection in station.branch_connections - ] - station_injection_assets = [ - injection_asset_map[asset_connection.asset_id].model_copy(deep=True) - for asset_connection in station.injection_connections - ] - - station_branch_asset_bays = [ - asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) - if asset_connection.asset_bay_id is not None - else None - for asset_connection in station.branch_connections - ] - station_injection_asset_bays = [ - asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) - if asset_connection.asset_bay_id is not None - else None - for asset_connection in station.injection_connections - ] - - materialized_stations.append( - MaterializedStation( - grid_model_id=station.grid_model_id, - name=station.name, - station_type=station.station_type, - region=station.region, - voltage_level=station.voltage_level, - busbars=station.busbars, - couplers=station.couplers, - branch_connections=[ - MaterializedAssetConnection( - asset=asset, - terminal=asset_connection.terminal, - asset_bay=asset_bay, - ) - for asset, asset_connection, asset_bay in zip( - station_branch_assets, - station.branch_connections, - station_branch_asset_bays, - strict=True, - ) - ], - injection_connections=[ - MaterializedAssetConnection( - asset=asset, - terminal=asset_connection.terminal, - asset_bay=asset_bay, - ) - for asset, asset_connection, asset_bay in zip( - station_injection_assets, - station.injection_connections, - station_injection_asset_bays, - strict=True, - ) - ], - branch_switching_table=station.branch_switching_table, - injection_switching_table=station.injection_switching_table, - branch_connectivity=station.branch_connectivity, - injection_connectivity=station.injection_connectivity, - model_log=station.model_log, - ) - ) - - return materialized_stations - - def get_asset_bay_ids_for_asset(self, asset_grid_model_id: str) -> list[str]: - """Return all station-scoped asset bay ids connected to a topology asset. - - Parameters - ---------- - asset_grid_model_id : str - Grid model id of the topology-owned asset. - - Returns - ------- - list[str] - Ordered unique asset bay ids connected to the asset across all raw stations. - """ - asset_bay_ids: list[str] = [] - seen_ids: set[str] = set() - for station in self.raw_stations: - for asset_connection in [*station.branch_connections, *station.injection_connections]: - asset_id = asset_connection.asset_id - asset_bay_id = asset_connection.asset_bay_id - if asset_id != asset_grid_model_id or asset_bay_id is None or asset_bay_id in seen_ids: - continue - seen_ids.add(asset_bay_id) - asset_bay_ids.append(asset_bay_id) - return asset_bay_ids - - def get_asset_bays_for_asset(self, asset_grid_model_id: str) -> list[AssetBay]: - """Return all station-scoped asset bays connected to a topology asset. - - Parameters - ---------- - asset_grid_model_id : str - Grid model id of the topology-owned asset. - - Returns - ------- - list[AssetBay] - Ordered unique asset bay payloads connected to the asset across all raw stations. - """ - asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} - return [asset_bay_map[asset_bay_id] for asset_bay_id in self.get_asset_bay_ids_for_asset(asset_grid_model_id)] - - def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "Topology": - """Copy and revalidate the topology.""" - payload = _merged_round_trip_payload(self, update, deep=deep) - return type(self).model_validate(payload) - - -def build_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, occurrence_index: int = 0) -> str: - """Create a deterministic station-scoped asset bay identifier. - - Parameters - ---------- - station_grid_model_id : str - Station identifier owning the asset bay. - asset_grid_model_id : str - Asset identifier for which the bay id is created. - occurrence_index : int, default=0 - Zero-based occurrence index for repeated asset ids within one station. - - Returns - ------- - str - Deterministic asset bay identifier scoped to the station. - """ - base_asset_bay_id = f"{station_grid_model_id}::{asset_grid_model_id}::bay" - if occurrence_index == 0: - return base_asset_bay_id - return f"{base_asset_bay_id}::{occurrence_index}" - - -def topology_parts_from_materialized_station( - station: MaterializedStation, -) -> tuple[RawStation, list[SwitchableAsset], list[AssetBay]]: - """Extract topology-owned payloads from a materialized station. - - Parameters - ---------- - station : MaterializedStation - Materialized station to decompose. - - Returns - ------- - tuple[RawStation, list[SwitchableAsset], list[AssetBay]] - Raw station record plus topology-owned assets and asset bays derived from the station. - """ - branch_assets: list[SwitchableAsset] = [] - injection_assets: list[SwitchableAsset] = [] - asset_bays: list[AssetBay] = [] - branch_connections: list[StationAssetConnection] = [] - injection_connections: list[StationAssetConnection] = [] - for asset_connection in station.branch_connections: - asset = normalize_switchable_asset_payload(asset_connection.asset.model_dump(round_trip=True)) - asset_bay = asset_connection.asset_bay - asset_id = asset.grid_model_id - asset_bay_id: Optional[str] = None - - if asset_bay is not None: - asset_bay_id = asset_bay.asset_bay_id - asset_bays.append(asset_bay.model_copy(deep=True)) - - branch_assets.append(asset.model_copy(deep=True)) - branch_connections.append( - StationAssetConnection( - asset_id=asset_id, - terminal=asset_connection.terminal, - asset_bay_id=asset_bay_id, - ) - ) - - for asset_connection in station.injection_connections: - asset = normalize_switchable_asset_payload(asset_connection.asset.model_dump(round_trip=True)) - asset_bay = asset_connection.asset_bay - asset_id = asset.grid_model_id - asset_bay_id = asset_bay.asset_bay_id if asset_bay is not None else None - - if asset_bay is not None: - asset_bays.append(asset_bay.model_copy(deep=True)) - - injection_assets.append(asset.model_copy(deep=True)) - injection_connections.append( - StationAssetConnection( - asset_id=asset_id, - terminal=asset_connection.terminal, - asset_bay_id=asset_bay_id, - ) - ) - - return ( - RawStation( - grid_model_id=station.grid_model_id, - name=station.name, - station_type=station.station_type, - region=station.region, - voltage_level=station.voltage_level, - busbars=station.busbars, - couplers=station.couplers, - branch_connections=branch_connections, - injection_connections=injection_connections, - branch_switching_table=station.branch_switching_table, - injection_switching_table=station.injection_switching_table, - branch_connectivity=station.branch_connectivity, - injection_connectivity=station.injection_connectivity, - model_log=station.model_log, - ), - [*branch_assets, *injection_assets], - asset_bays, - ) - - -def topology_from_materialized_stations(reference_topology: Topology, stations: list[MaterializedStation]) -> Topology: - """Create a topology from materialized stations. - - Parameters - ---------- - reference_topology : Topology - Reference topology providing shared metadata. - stations : list[MaterializedStation] - Materialized stations to convert. - - Returns - ------- - Topology - Topology containing raw stations and topology-owned payloads derived from ``stations``. - """ - topology_stations: list[RawStation] = [] - topology_branch_assets: dict[str, SwitchableAsset] = {} - topology_injection_assets: dict[str, SwitchableAsset] = {} - topology_asset_bays: dict[str, AssetBay] = {} - - for station in stations: - topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) - topology_stations.append(topology_station) - for asset in station_assets: - topology_assets = topology_branch_assets if asset.is_branch() is not False else topology_injection_assets - existing_asset = topology_assets.get(asset.grid_model_id) - if existing_asset is None: - topology_assets[asset.grid_model_id] = asset - elif existing_asset != asset: - raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") - for asset_bay in station_asset_bays: - existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) - if existing_asset_bay is None: - topology_asset_bays[asset_bay.asset_bay_id] = asset_bay - elif existing_asset_bay != asset_bay: - raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") - - return Topology( - topology_id=reference_topology.topology_id, - grid_model_file=reference_topology.grid_model_file, - name=reference_topology.name, - raw_stations=topology_stations, - branch_assets=list(topology_branch_assets.values()), - injection_assets=list(topology_injection_assets.values()), - asset_bays=list(topology_asset_bays.values()), - asset_setpoints=reference_topology.asset_setpoints, - timestamp=reference_topology.timestamp, - metrics=reference_topology.metrics, - ) - - -def copy_topology_with_updates( - reference_topology: Topology, - raw_stations: list[RawStation], - asset_bays: list[AssetBay], - *, - branch_assets: Optional[list[SwitchableAsset]] = None, - injection_assets: Optional[list[SwitchableAsset]] = None, -) -> Topology: - """Create a validated topology copy with updated payloads. - - Parameters - ---------- - reference_topology : Topology - Reference topology providing shared metadata. - raw_stations : list[RawStation] - Raw stations to set on the copied topology. - asset_bays : list[AssetBay] - Topology-owned asset bays to set on the copied topology. - branch_assets : Optional[list[SwitchableAsset]], optional - Topology-owned branch assets to set on the copied topology. - injection_assets : Optional[list[SwitchableAsset]], optional - Topology-owned injection assets to set on the copied topology. - - Returns - ------- - Topology - Validated topology copy with updated topology-owned payloads. - """ - resolved_branch_assets = branch_assets if branch_assets is not None else reference_topology.branch_assets - resolved_injection_assets = injection_assets if injection_assets is not None else reference_topology.injection_assets - - return Topology( - topology_id=reference_topology.topology_id, - grid_model_file=reference_topology.grid_model_file, - name=reference_topology.name, - raw_stations=raw_stations, - branch_assets=resolved_branch_assets, - injection_assets=resolved_injection_assets, - asset_bays=asset_bays, - asset_setpoints=reference_topology.asset_setpoints, - timestamp=reference_topology.timestamp, - metrics=reference_topology.metrics, - ) - - -class Strategy(BaseModel): - """Timestep data describing a collection of single timesteps, each represented by a Topology.""" - - strategy_id: str - """ The unique identifier of the strategy. """ - - timesteps: list[Topology] - """ The list of topologies, one for every timestep. """ - - name: Optional[str] = None - """ The name of the strategy. """ - - author: Optional[str] = None - """ The author of the strategy, i.e. who has created it. """ - - process_type: Optional[str] = None - """ The process type that created this topology, e.g. DC-solver, DC+-solver, Human etc. """ - - process_parameters: Optional[dict[str, Union[str, float]]] = None - """ The process parameters that were used to create this topology.""" - - date_of_creation: Optional[datetime] = None - """ The date of creation of this strategy, i.e. when the optimization ran. """ - - metadata: Optional[dict[str, Any]] = None - """ Additional metadata that might be useful for the strategy. """ - - -class AppliedStation(BaseModel): - """A realized station, including the new station and the changes made to the original station.""" - - station: MaterializedStation - """The realized asset station object""" - - coupler_diff: list[BusbarCoupler] - """A list of couplers that have been switched.""" - - branch_reassignment_diff: list[tuple[int, int, bool]] - """Branch reassignments as ``(branch_index, busbar_index, connected)`` tuples.""" - - injection_reassignment_diff: list[tuple[int, int, bool]] - """Injection reassignments as ``(injection_index, busbar_index, connected)`` tuples.""" - - branch_disconnection_diff: list[int] - """Branch indices that were disconnected.""" - - injection_disconnection_diff: list[int] - """Injection indices that were disconnected.""" - - -class RealizedTopology(BaseModel): - """A realized topology, including the new topology and the changes made to the original topology. - - This is similar to AppliedStation but holding information for all stations in the topology. - The diffs are include a station identifier that shows which station in the topology was affected by the - diff. - """ - - topology: Topology - """The realized asset topology object""" - - coupler_diff: list[tuple[str, BusbarCoupler]] - """A list of couplers that have been switched. Each tuple contains the station grid_model_id - and the coupler that was switched.""" - - branch_reassignment_diff: list[tuple[str, int, int, bool]] - """Branch reassignments as ``(station_id, branch_index, busbar_index, connected)`` tuples.""" - - injection_reassignment_diff: list[tuple[str, int, int, bool]] - """Injection reassignments as ``(station_id, injection_index, busbar_index, connected)`` tuples.""" - - branch_disconnection_diff: list[tuple[str, int]] - """Branch disconnections as ``(station_id, branch_index)`` tuples.""" - - injection_disconnection_diff: list[tuple[str, int]] - """Injection disconnections as ``(station_id, injection_index)`` tuples.""" diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/applied_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/applied_topology.py new file mode 100644 index 000000000..3dc51f026 --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/applied_topology.py @@ -0,0 +1,67 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Classes that represent the applied topology. + +This is the station-local view of the topology +with assets and asset bays, including the differences to the original topology. +""" + +from pydantic import BaseModel +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.assets import BusbarCoupler +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation + + +class RealizedTopology(BaseModel): + """A realized topology, including the new topology and the changes made to the original topology. + + This is similar to AppliedStation but holding information for all stations in the topology. + The diffs are include a station identifier that shows which station in the topology was affected by the + diff. + """ + + topology: Topology + """The realized asset topology object""" + + coupler_diff: list[tuple[str, BusbarCoupler]] + """A list of couplers that have been switched. Each tuple contains the station grid_model_id + and the coupler that was switched.""" + + branch_reassignment_diff: list[tuple[str, int, int, bool]] + """Branch reassignments as ``(station_id, branch_index, busbar_index, connected)`` tuples.""" + + injection_reassignment_diff: list[tuple[str, int, int, bool]] + """Injection reassignments as ``(station_id, injection_index, busbar_index, connected)`` tuples.""" + + branch_disconnection_diff: list[tuple[str, int]] + """Branch disconnections as ``(station_id, branch_index)`` tuples.""" + + injection_disconnection_diff: list[tuple[str, int]] + """Injection disconnections as ``(station_id, injection_index)`` tuples.""" + + +class AppliedStation(BaseModel): + """A realized station, including the new station and the changes made to the original station.""" + + station: MaterializedStation + """The realized asset station object""" + + coupler_diff: list[BusbarCoupler] + """A list of couplers that have been switched.""" + + branch_reassignment_diff: list[tuple[int, int, bool]] + """Branch reassignments as ``(branch_index, busbar_index, connected)`` tuples.""" + + injection_reassignment_diff: list[tuple[int, int, bool]] + """Injection reassignments as ``(injection_index, busbar_index, connected)`` tuples.""" + + branch_disconnection_diff: list[int] + """Branch indices that were disconnected.""" + + injection_disconnection_diff: list[int] + """Injection indices that were disconnected.""" diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py new file mode 100644 index 000000000..b204b1f93 --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py @@ -0,0 +1,348 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Contains the data models for the asset topology.""" + +from datetime import datetime + +from beartype.typing import Any, Optional, Union +from pydantic import BaseModel, Field, field_validator, model_validator +from toop_engine_interfaces.asset_topology.assets import AssetBay, AssetSetpoint, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import ( + RawStation, + _merged_round_trip_payload, +) + + +class CircuitGroup(BaseModel): + """A circuit group represents a set of assets that are connected to each other without power switches. + + This means in case of an outage, the fault current can flow through all assets in the same circuit group, + triggering their outage aswell. + + -> All assets in an asset group are outaged together. + + # TODO: This is currently not implemented. Use a graph search to determine these. + """ + + asset_ids: list[str] + """The grid model ids of the assets in the circuit group. + This can be used to quickly find the circuit group in case an asset with id x is outaged.""" + + asset_bay_ids: list[str] + """The asset bay ids of the asset bays in the circuit group. + These can be used to apply the outage effect on the grid by opening the switches in the asset bays.""" + + +class Topology(BaseModel): + """Topology data describing a single timestep topology. + + A topology includes lean station records in raw_stations, topology-owned canonical assets and + asset bays, and potentially asset setpoints. + Use materialize_stations() to reconstruct rich Station objects. + """ + + topology_id: str + """ The unique identifier of the topology. """ + + grid_model_file: Optional[str] = None + """ The grid model file that represents this timestep. Note that relevant folders might only + work on the machine they have been created, so some sort of permanent storage server should be + used to keep these files globally accessible""" + + name: Optional[str] = None + """ The name of the topology. """ + + raw_stations: list[RawStation] + """The topology-owned station records without embedded asset payloads. + + Each raw station represents one bus-branch bus view of a splitable station. + """ + + branch_assets: list[SwitchableAsset] = Field(default_factory=list) + """The topology-owned canonical branch payloads.""" + + injection_assets: list[SwitchableAsset] = Field(default_factory=list) + """The topology-owned canonical injection payloads. + + Station-local branch-end and asset-bay assignment data are stored on raw_stations instead of on + these canonical payloads. + """ + + asset_bays: list[AssetBay] = Field(default_factory=list) + """The topology-owned asset bay payloads.""" + + asset_setpoints: Optional[list[AssetSetpoint]] = None + """ The list of asset setpoints in the topology. """ + + timestamp: datetime + """ The timestamp which is represented by this topology during the original optimization. I.e. + if this timestep was the 5 o clock timestep on the day that was optimized, then this timestamp + would read 5 o clock. """ + + metrics: Optional[dict[str, float]] = None + """ The metrics of the topology. """ + + @field_validator("branch_assets") + @classmethod + def check_branch_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + """Check if all topology branch assets have unique grid model ids.""" + asset_ids = [asset.grid_model_id for asset in v] + if len(asset_ids) != len(set(asset_ids)): + raise ValueError("grid_model_id must be unique for topology branch assets") + if any(asset.is_branch() is False for asset in v): + raise ValueError("branch_assets must not contain injection assets") + return v + + @field_validator("injection_assets") + @classmethod + def check_injection_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + """Check if all topology injection assets have unique grid model ids.""" + asset_ids = [asset.grid_model_id for asset in v] + if len(asset_ids) != len(set(asset_ids)): + raise ValueError("grid_model_id must be unique for topology injection assets") + if any(asset.is_branch() is True for asset in v): + raise ValueError("injection_assets must not contain branch assets") + return v + + @field_validator("asset_bays") + @classmethod + def check_asset_bay_ids_unique(cls, v: list[AssetBay]) -> list[AssetBay]: + """Check if all topology asset bay ids are unique.""" + asset_bay_ids = [asset_bay.asset_bay_id for asset_bay in v] + if any(asset_bay_id is None for asset_bay_id in asset_bay_ids): + raise ValueError("All topology asset bays must define asset_bay_id") + if len(asset_bay_ids) != len(set(asset_bay_ids)): + raise ValueError("asset_bay_id must be unique for topology asset bays") + return v + + @model_validator(mode="after") + def check_station_asset_references(self: "Topology") -> "Topology": + """Check if all station asset references exist in the topology-owned collections.""" + branch_asset_ids = {asset.grid_model_id for asset in self.branch_assets} + injection_asset_ids = {asset.grid_model_id for asset in self.injection_assets} + asset_bay_ids = {asset_bay.asset_bay_id for asset_bay in self.asset_bays} + + for station in self.raw_stations: + for asset_connection in station.branch_connections: + asset_id = asset_connection.asset_id + if asset_id not in branch_asset_ids: + raise ValueError( + f"Branch asset grid_model_id {asset_id} referenced by station " + f"{station.grid_model_id} does not exist in topology assets" + ) + for asset_connection in station.injection_connections: + asset_id = asset_connection.asset_id + if asset_id not in injection_asset_ids: + raise ValueError( + f"Injection asset grid_model_id {asset_id} referenced by station " + f"{station.grid_model_id} does not exist in topology assets" + ) + for asset_connection in [*station.branch_connections, *station.injection_connections]: + asset_bay_id = asset_connection.asset_bay_id + if asset_bay_id is not None and asset_bay_id not in asset_bay_ids: + raise ValueError( + f"asset_bay_id {asset_bay_id} referenced by station " + f"{station.grid_model_id} does not exist in topology asset bays" + ) + + return self + + def materialize_stations(self) -> list[MaterializedStation]: + """Materialize station-local asset payloads from topology-owned assets and asset bays.""" + branch_asset_map = {asset.grid_model_id: asset for asset in self.branch_assets} + injection_asset_map = {asset.grid_model_id: asset for asset in self.injection_assets} + asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} + materialized_stations: list[MaterializedStation] = [] + + for station in self.raw_stations: + station_branch_assets = [ + branch_asset_map[asset_connection.asset_id].model_copy(deep=True) + for asset_connection in station.branch_connections + ] + station_injection_assets = [ + injection_asset_map[asset_connection.asset_id].model_copy(deep=True) + for asset_connection in station.injection_connections + ] + + station_branch_asset_bays = [ + asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) + if asset_connection.asset_bay_id is not None + else None + for asset_connection in station.branch_connections + ] + station_injection_asset_bays = [ + asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) + if asset_connection.asset_bay_id is not None + else None + for asset_connection in station.injection_connections + ] + + materialized_stations.append( + MaterializedStation( + grid_model_id=station.grid_model_id, + name=station.name, + station_type=station.station_type, + region=station.region, + voltage_level=station.voltage_level, + busbars=station.busbars, + couplers=station.couplers, + branch_connections=[ + MaterializedAssetConnection( + asset=asset, + terminal=asset_connection.terminal, + asset_bay=asset_bay, + ) + for asset, asset_connection, asset_bay in zip( + station_branch_assets, + station.branch_connections, + station_branch_asset_bays, + strict=True, + ) + ], + injection_connections=[ + MaterializedAssetConnection( + asset=asset, + terminal=asset_connection.terminal, + asset_bay=asset_bay, + ) + for asset, asset_connection, asset_bay in zip( + station_injection_assets, + station.injection_connections, + station_injection_asset_bays, + strict=True, + ) + ], + branch_switching_table=station.branch_switching_table, + injection_switching_table=station.injection_switching_table, + branch_connectivity=station.branch_connectivity, + injection_connectivity=station.injection_connectivity, + model_log=station.model_log, + ) + ) + + return materialized_stations + + def get_asset_bay_ids_for_asset(self, asset_grid_model_id: str) -> list[str]: + """Return all station-scoped asset bay ids connected to a topology asset. + + Parameters + ---------- + asset_grid_model_id : str + Grid model id of the topology-owned asset. + + Returns + ------- + list[str] + Ordered unique asset bay ids connected to the asset across all raw stations. + """ + asset_bay_ids: list[str] = [] + seen_ids: set[str] = set() + for station in self.raw_stations: + for asset_connection in [*station.branch_connections, *station.injection_connections]: + asset_id = asset_connection.asset_id + asset_bay_id = asset_connection.asset_bay_id + if asset_id != asset_grid_model_id or asset_bay_id is None or asset_bay_id in seen_ids: + continue + seen_ids.add(asset_bay_id) + asset_bay_ids.append(asset_bay_id) + return asset_bay_ids + + def get_asset_bays_for_asset(self, asset_grid_model_id: str) -> list[AssetBay]: + """Return all station-scoped asset bays connected to a topology asset. + + Parameters + ---------- + asset_grid_model_id : str + Grid model id of the topology-owned asset. + + Returns + ------- + list[AssetBay] + Ordered unique asset bay payloads connected to the asset across all raw stations. + """ + asset_bay_map = {asset_bay.asset_bay_id: asset_bay for asset_bay in self.asset_bays} + return [asset_bay_map[asset_bay_id] for asset_bay_id in self.get_asset_bay_ids_for_asset(asset_grid_model_id)] + + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "Topology": + """Copy and revalidate the topology.""" + payload = _merged_round_trip_payload(self, update, deep=deep) + return type(self).model_validate(payload) + + +def copy_topology_with_updates( + reference_topology: Topology, + raw_stations: list[RawStation], + asset_bays: list[AssetBay], + *, + branch_assets: Optional[list[SwitchableAsset]] = None, + injection_assets: Optional[list[SwitchableAsset]] = None, +) -> Topology: + """Create a validated topology copy with updated payloads. + + Parameters + ---------- + reference_topology : Topology + Reference topology providing shared metadata. + raw_stations : list[RawStation] + Raw stations to set on the copied topology. + asset_bays : list[AssetBay] + Topology-owned asset bays to set on the copied topology. + branch_assets : Optional[list[SwitchableAsset]], optional + Topology-owned branch assets to set on the copied topology. + injection_assets : Optional[list[SwitchableAsset]], optional + Topology-owned injection assets to set on the copied topology. + + Returns + ------- + Topology + Validated topology copy with updated topology-owned payloads. + """ + resolved_branch_assets = branch_assets if branch_assets is not None else reference_topology.branch_assets + resolved_injection_assets = injection_assets if injection_assets is not None else reference_topology.injection_assets + + return Topology( + topology_id=reference_topology.topology_id, + grid_model_file=reference_topology.grid_model_file, + name=reference_topology.name, + raw_stations=raw_stations, + branch_assets=resolved_branch_assets, + injection_assets=resolved_injection_assets, + asset_bays=asset_bays, + asset_setpoints=reference_topology.asset_setpoints, + timestamp=reference_topology.timestamp, + metrics=reference_topology.metrics, + ) + + +class Strategy(BaseModel): + """Timestep data describing a collection of single timesteps, each represented by a Topology.""" + + strategy_id: str + """ The unique identifier of the strategy. """ + + timesteps: list[Topology] + """ The list of topologies, one for every timestep. """ + + name: Optional[str] = None + """ The name of the strategy. """ + + author: Optional[str] = None + """ The author of the strategy, i.e. who has created it. """ + + process_type: Optional[str] = None + """ The process type that created this topology, e.g. DC-solver, DC+-solver, Human etc. """ + + process_parameters: Optional[dict[str, Union[str, float]]] = None + """ The process parameters that were used to create this topology.""" + + date_of_creation: Optional[datetime] = None + """ The date of creation of this strategy, i.e. when the optimization ran. """ + + metadata: Optional[dict[str, Any]] = None + """ Additional metadata that might be useful for the strategy. """ diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology_helpers.py similarity index 99% rename from packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py rename to packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology_helpers.py index 5de71c319..fde7d7e82 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology_helpers.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology_helpers.py @@ -16,18 +16,13 @@ from beartype.typing import Literal, Optional, Union from fsspec import AbstractFileSystem from fsspec.implementations.local import LocalFileSystem -from toop_engine_interfaces.asset_topology import ( - AppliedStation, - AssetBay, - Busbar, - BusbarCoupler, - MaterializedAssetConnection, - MaterializedStation, - RealizedTopology, - SwitchableAsset, +from toop_engine_interfaces.asset_topology.applied_topology import AppliedStation, RealizedTopology +from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, copy_topology_with_updates, ) +from toop_engine_interfaces.asset_topology.assets import AssetBay, Busbar, BusbarCoupler, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.filesystem_helper import load_pydantic_model_fs diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_types.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_types.py new file mode 100644 index 000000000..82dfc323b --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_types.py @@ -0,0 +1,53 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Shared type aliases for asset topology models.""" + +from beartype.typing import Literal, TypeAlias + +BranchEnd: TypeAlias = Literal["from", "to", "hv", "mv", "lv"] +AssetBranchTypePandapower: TypeAlias = Literal[ + "line", + "trafo", + "trafo3w_lv", + "trafo3w_mv", + "trafo3w_hv", + "impedance", + "xward", +] +AssetBranchTypePowsybl: TypeAlias = Literal[ + "LINE", + "TWO_WINDINGS_TRANSFORMER", + "TIE_LINE", +] +AssetBranchType: TypeAlias = Literal[AssetBranchTypePandapower, AssetBranchTypePowsybl] + +AssetInjectionTypePandapower: TypeAlias = Literal[ + "ext_grid", + "gen", + "load", + "shunt", + "sgen", + "ward", + "ward_load", + "ward_shunt", + "xward_load", + "xward_shunt", + "dcline_from", + "dcline_to", +] +AssetInjectionTypePowsybl: TypeAlias = Literal[ + "LOAD", + "GENERATOR", + "BOUNDARY_LINE", + "HVDC_CONVERTER_STATION", + "STATIC_VAR_COMPENSATOR", + "SHUNT_COMPENSATOR", + "BATTERY", +] +AssetInjectionType: TypeAlias = Literal[AssetInjectionTypePandapower, AssetInjectionTypePowsybl] +AssetType: TypeAlias = Literal[AssetBranchType, AssetInjectionType] diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py new file mode 100644 index 000000000..9d1d55526 --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py @@ -0,0 +1,303 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Classes that represent Assets in the grid""" + +from enum import Enum + +from beartype.typing import Any, Optional, get_args +from pydantic import BaseModel, field_validator +from toop_engine_interfaces.asset_topology.asset_types import AssetBranchType, AssetInjectionType, AssetType + + +class Busbar(BaseModel): + """Busbar data describing a single busbar a station.""" + + grid_model_id: str + """ The unique identifier of the busbar. + Corresponds to the busbar's id in the grid model.""" + + busbar_type: Optional[str] = None + """ The type of the busbar, might be useful for finding the busbar later on """ + + name: Optional[str] = None + """ The name of the busbar, might be useful for finding the busbar later on """ + + int_id: int + """ Is used to reference busbars in the couplers. Needs to be unique per station""" + + in_service: bool = True + """ Whether the busbar is in service. If False, it will be ignored in the switching table""" + + bus_branch_bus_id: Optional[str] = None + """ The bus_branch_bus_id refers to the bus-branch model bus id. + There might be a difference between the busbar grid_model_id (a physical busbar) + and the bus_branch_bus_id from the bus-branch model. + Use this bus_branch_bus_id to store the bus-branch model bus id. + Note: the Station grid_model_id also a bus-branch bus_branch_bus_id. This id is the most splitable bus_branch_bus_id. + Other bus_branch_bus_ids are part of the physical station, but are separated by a coupler or branch.""" + + +class AssetBay(BaseModel): + """Saves the physical connection from the asset to the substation busbars - a bay (Schaltfeld). + + A line usually has three switches, before it is connected to the busbar. + Two disconnector switches and one circuit breaker switch. + A transformer usually has two switches, before it is connected to the busbar. + One disconnector switch and one circuit breaker switch. + + type: n - node + type: b - busbar (Sammelschiene) + type: CB - DV Circuit Breaker / Power Switch (Leistungsschalter) + type: DS - Disconnector Switch (Trennschalter) + + ------------------ busbar 1 - type: b + | + / type: DS - SR Switch busbar 1 -> used for reassigning the asset to another busbar + | + ------|----------- busbar 2 - type: b + | | + / | type: DS - SR Switch busbar 2 -> used for reassigning the asset to another busbar + | | + --------- bus_3 - type: n - busbar section bus + | + / type: CB - DV Circuit Breaker / Power Switch -> used for disconnecting the asset from the busbar + | + --------- bus_2 - type: n - circuit breaker bus + | + / type: DS - SL Switch (optional) -> not used by the asset + | + --------- bus_1 - type: n - asset bus + ^ + | Line/Transformer + + + """ + + asset_bay_id: str + """Topology-scoped identifier for the asset bay.""" + + sl_switch_grid_model_id: Optional[str] = None + """ The id of the switch, which connects the asset to the circuit breaker node. + This switch is a disconnector switch. Do not use for anything, leave state as found. + Default should be closed.""" + + dv_switch_grid_model_id: str + """ This switch is a circuit breaker / power switch. + Use for disconnecting / reconnecting the asset from the busbar. """ + + sr_switch_grid_model_id: dict[str, str] + """ The ids of the switches, which assign the asset to the busbars. + key: busbar_grid_model_id e.g. 4%%bus + value: sr_switch_grid_model_id + This switch is a disconnector switch. Use for reassigning the asset to another busbar. + Only one switch should be closed at a time. + """ + + @field_validator("sr_switch_grid_model_id") + @classmethod + def check_is_empty(cls, v: dict[str, str]) -> dict[str, str]: + """Check if the dict is empty. + + Parameters + ---------- + v : dict[str, str] + The dictionary of sr_switch_grid_model_id to check. + + Returns + ------- + dict[str, str] + The dictionary itself. + + Raises + ------ + ValueError + If the dictionary is empty. + """ + if len(v) == 0: + raise ValueError("sr_switch_grid_model_id must not be empty") + return v + + +class BusbarCoupler(BaseModel): + """Coupler data describing a single coupler at a station. + + This references only busbar couplers, i.e. couplers connecting two busbars. + Switches connecting assets to a busbar are represented in the asset_switching_table in the station model. + + Note: A busbar couple is a physical connection between two busbars, this can be also a + cross coupler. To further specify the connection of an asset to a busbar, the asset connection + """ + + grid_model_id: str + """ The unique identifier of the coupler. + Corresponds to the coupler's id in the grid model.""" + + coupler_type: Optional[str] = None + """ The type of the coupler, might be useful for finding the coupler later on """ + + name: Optional[str] = None + """ The name of the coupler, might be useful for finding the coupler later on """ + + # TODO: this does not work for a coupler with multiple busbars on one side + busbar_from_id: int + """ Is used to determine where the coupler is connected to the busbars on the "from" side. + Refers to the int_id of the busbar""" + + # TODO: this does not work for a coupler with multiple busbars on one side + busbar_to_id: int + """ Is used to determine where the coupler is connected to the busbars on the "to" side. + Refers to the int_id of the busbar""" + + open: bool + """ The status of the coupler. True if the coupler is open, False if the coupler is closed. + TODO: Switch to using the connectivity table instead of this field. + """ + + in_service: bool = True + """ Whether the coupler is in-service. Out-of-service couplers are assumed to be always open""" + + asset_bay: Optional[AssetBay] = None + """ The asset bay (Schaltfeld) of the coupler. + Note: A coupler can have multiple from and to busbars. + The asset bay sr_switch_grid_model_id is used save the selector switches of the coupler. + Note: A coupler has never a sl_switch_grid_model_id, the dv_switch_grid_model_id should + the same as the name of the coupler. + + """ + + +class SwitchableAsset(BaseModel): + """Asset data describing a single asset at a station. + + An asset can be for instance a transformer, line, generator, load, shunt. + Note: An asset can be connected to multiple busbars through the switching grid, however if + this happens a closed coupler between these busbars is assumed. If such couplers are not present, + they will be created. + Note: An asset that is out-of-service can be represented, but its switching entries will be + ignored. + """ + + grid_model_id: str + """ The unique identifier of the asset. + Corresponds to the asset's id in the grid model.""" + + asset_type: Optional[AssetType] = None + """ The type of the asset. These refer loosely to the types in the pandapower/powsybl grid + models. If set, this can be used to disambiguate branches from injections """ + + name: Optional[str] = None + """ The name of the asset, might be useful for finding the asset later on """ + + in_service: bool = True + """ If the element is in service. False means the switching entry for this element will be + ignored. This shall not be used for elements intentionally disconnected, instead set all zeros + in the switching table.""" + + def is_branch(self) -> Optional[bool]: + """Return True if the asset is a branch. + + Only works if the type is set. If type is not set this will return None. + + Returns + ------- + bool + True if the asset is a branch, False if it is an injection. + """ + if self.asset_type is None: + return None + return self.asset_type in get_args(AssetBranchType) + + +class BranchAsset(SwitchableAsset): + """Switchable asset representing a branch-type element.""" + + asset_type: Optional[AssetBranchType] = None + + def is_branch(self) -> Optional[bool]: + """Return branch semantics for branch assets.""" + return None if self.asset_type is None else True + + +class InjectionAsset(SwitchableAsset): + """Switchable asset representing an injection-type element.""" + + asset_type: Optional[AssetInjectionType] = None + + def is_branch(self) -> Optional[bool]: + """Return branch semantics for injection assets.""" + return None if self.asset_type is None else False + + +def normalize_switchable_asset_payload(asset: dict[str, Any]) -> SwitchableAsset: + """Normalize an asset payload to the matching branch or injection subclass when possible.""" + if isinstance(asset, (BranchAsset, InjectionAsset)): + return asset + + asset_data = asset.model_dump() if isinstance(asset, SwitchableAsset) else dict(asset) + if "asset_type" not in asset_data and "type" in asset_data: + asset_data["asset_type"] = asset_data.pop("type") + asset_type = asset_data.get("asset_type") + if asset_type in get_args(AssetBranchType): + return BranchAsset(**asset_data) + if asset_type in get_args(AssetInjectionType): + return InjectionAsset(**asset_data) + return SwitchableAsset(**asset_data) + + +class AssetSetpoint(BaseModel): + """Asset data describing a single asset with a setpoint. + + This could for example be a PST or HVDC setpoint. + Note: The same asset can both be switchable and have a setpoint. In this case, the asset will + be represented twice. + """ + + grid_model_id: str + """ The unique identifier of the asset. + Corresponds to the asset's id in the grid model.""" + + asset_type: Optional[str] = None + """ The type of the asset, might be useful for finding the asset later on """ + + name: Optional[str] = None + """ The name of the asset, might be useful for finding the asset later on """ + + setpoint: float + """ The setpoint of the asset. """ + + +def build_asset_bay_id(station_grid_model_id: str, asset_grid_model_id: str, occurrence_index: int = 0) -> str: + """Create a deterministic station-scoped asset bay identifier. + + Parameters + ---------- + station_grid_model_id : str + Station identifier owning the asset bay. + asset_grid_model_id : str + Asset identifier for which the bay id is created. + occurrence_index : int, default=0 + Zero-based occurrence index for repeated asset ids within one station. + + Returns + ------- + str + Deterministic asset bay identifier scoped to the station. + """ + base_asset_bay_id = f"{station_grid_model_id}::{asset_grid_model_id}::bay" + if occurrence_index == 0: + return base_asset_bay_id + return f"{base_asset_bay_id}::{occurrence_index}" + + +class PowsyblSwitchValues(Enum): + """Enum for the switch values in the Powsybl model.""" + + OPEN = True + """ The switch is open, i.e. not connected.""" + CLOSED = False + """ The switch is closed, i.e. connected.""" diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py new file mode 100644 index 000000000..824b429e1 --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py @@ -0,0 +1,203 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Classes that represent the materialized topology. + +This is the station-local view of the topology populated with all necessary info +on assets and asset bays. +""" + +import numpy as np +from beartype.typing import Any, Literal, Optional +from pydantic import BaseModel, Field, model_validator +from toop_engine_interfaces.asset_topology.asset_types import BranchEnd +from toop_engine_interfaces.asset_topology.assets import AssetBay, SwitchableAsset +from toop_engine_interfaces.asset_topology.station_models import ( + _merged_round_trip_payload, + _StationStructure, + _validate_station_physical_assignments, + _validate_station_switching_tables, +) + + +class MaterializedAssetConnection(BaseModel): + """Station-local association between a switching-table column and a materialized asset payload.""" + + asset: SwitchableAsset + """Station-local asset payload aligned with one switching-table column.""" + + terminal: Optional[BranchEnd] = None + """Optional branch terminal metadata for this station-local asset occurrence.""" + + asset_bay: Optional[AssetBay] = None + """Optional station-local asset bay payload for this station-local asset occurrence.""" + + def get_sr_switch(self) -> Optional[dict[str, str]]: + """Return the sr_switch_grid_model_id dict from the asset bay if it exists.""" + if self.asset_bay is not None: + return self.asset_bay.sr_switch_grid_model_id + return None + + +class MaterializedStation(_StationStructure): + """Station data describing a single materialized station. + + The station identity refers to a single bus-branch model bus_id that represents one splitable + station view. + A physical substation or voltage level may contain multiple bus-branch model bus_ids. + The station assets are aligned with the switching tables and describe the assets visible in that + station view; they are not intended to define a topology-owned canonical asset list. + """ + + branch_connections: list[MaterializedAssetConnection] = Field(default_factory=list) + """Station-local branch payloads aligned with ``branch_switching_table``.""" + + injection_connections: list[MaterializedAssetConnection] = Field(default_factory=list) + """Station-local injection payloads aligned with ``injection_switching_table``.""" + + @model_validator(mode="after") + def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": + """Check if switching-table-aligned station-local assets match the matrix shapes.""" + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.branch_connections), + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", + ) + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.injection_connections), + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", + ) + return self + + @model_validator(mode="after") + def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": + """Check if the asset bay bus is in busbars.""" + busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] + for asset_connection in [*self.branch_connections, *self.injection_connections]: + asset = asset_connection.asset + asset_bay = asset_connection.asset_bay + if asset_bay is not None: + for busbar_id in asset_bay.sr_switch_grid_model_id.keys(): + if busbar_id not in busbar_grid_model_id: + raise ValueError( + f"busbar_id {busbar_id} in asset {asset.grid_model_id} does not exist in busbars" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + + return self + + def __eq__(self, other: object) -> bool: + """Check if two stations are equal. + + Parameters + ---------- + other : object + The other station to compare to. + + Returns + ------- + bool + True if the stations are equal, False otherwise. + """ + if not isinstance(other, MaterializedStation): + return False + return ( + self.grid_model_id == other.grid_model_id + and self.region == other.region + and self.busbars == other.busbars + and self.couplers == other.couplers + and self.branch_connections == other.branch_connections + and self.injection_connections == other.injection_connections + and np.array_equal(self.branch_switching_table, other.branch_switching_table) + and np.array_equal(self.injection_switching_table, other.injection_switching_table) + and ( + np.array_equal(self.branch_connectivity, other.branch_connectivity) + if (self.branch_connectivity is not None and other.branch_connectivity is not None) + else self.branch_connectivity == other.branch_connectivity + ) + and ( + np.array_equal(self.injection_connectivity, other.injection_connectivity) + if (self.injection_connectivity is not None and other.injection_connectivity is not None) + else self.injection_connectivity == other.injection_connectivity + ) + ) + + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "MaterializedStation": + """Copy and revalidate the station.""" + payload = _merged_round_trip_payload(self, update, deep=deep) + return type(self).model_validate(payload) + + def get_connected_assets( + self, + busbar_index: int, + topology_assets: Optional[list[SwitchableAsset]] = None, + asset_scope: Literal["all", "branch", "injection"] = "all", + ) -> list[SwitchableAsset]: + """Return in-service assets connected to one busbar. + + Parameters + ---------- + busbar_index : int + Row index into the station switching tables. + topology_assets : Optional[list[SwitchableAsset]] + Ignored for materialized stations because payloads are embedded locally. + asset_scope : Literal["all", "branch", "injection"] + Restrict the lookup to branch or injection connections. + + Returns + ------- + list[SwitchableAsset] + Connected in-service assets for the requested busbar and scope. + """ + del topology_assets + if asset_scope == "branch": + return [ + asset_connection.asset + for asset_connection, is_connected in zip( + self.branch_connections, + self.branch_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_connection.asset.in_service + ] + if asset_scope == "injection": + return [ + asset_connection.asset + for asset_connection, is_connected in zip( + self.injection_connections, + self.injection_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_connection.asset.in_service + ] + return [ + *self.get_connected_assets(busbar_index, asset_scope="branch"), + *self.get_connected_assets(busbar_index, asset_scope="injection"), + ] diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py new file mode 100644 index 000000000..aaf08d402 --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py @@ -0,0 +1,435 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Shared station models for raw and materialized asset topologies.""" + +from copy import deepcopy + +import numpy as np +from beartype.typing import Any, Collection, Literal, Optional, TypeAlias +from numpydantic import NDArray, Shape +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from toop_engine_interfaces.asset_topology.asset_types import BranchEnd +from toop_engine_interfaces.asset_topology.assets import Busbar, BusbarCoupler, SwitchableAsset + +StationSwitchingArray: TypeAlias = NDArray[Shape["* n_bus, * n_asset"], np.bool_] + + +def _merged_round_trip_payload(model: BaseModel, update: Optional[dict[str, Any]], *, deep: bool = False) -> dict[str, Any]: + """Merge model field values and requested updates for revalidation-aware model_copy overrides.""" + payload = {field_name: getattr(model, field_name) for field_name in type(model).model_fields} + if deep: + payload = deepcopy(payload) + if update: + payload.update(update) + return payload + + +class StationAssetConnection(BaseModel): + """Station-local association between a switching-table column and a topology asset.""" + + asset_id: str + """Grid model id of the topology-owned asset referenced by this station-local column.""" + + terminal: Optional[BranchEnd] = None + """Optional branch terminal metadata for this station-local asset occurrence.""" + + asset_bay_id: Optional[str] = None + """Optional topology-scoped asset bay identifier for this station-local asset occurrence.""" + + +def _validate_station_switching_tables( + station_grid_model_id: str, + station_name: Optional[str], + busbar_count: int, + asset_count: int, + asset_switching_table: np.ndarray, + asset_connectivity: Optional[np.ndarray], + asset_kind: str, +) -> None: + """Validate switching-table shapes against the station dimensions. + + Parameters + ---------- + station_grid_model_id : str + Grid model id of the station being validated. + station_name : Optional[str] + Human-readable station name used in validation errors. + busbar_count : int + Expected number of busbar rows in the switching tables. + asset_count : int + Expected number of asset columns in the switching tables. + asset_switching_table : np.ndarray + Current station switching table. + asset_connectivity : Optional[np.ndarray] + Optional connectivity mask describing physically allowed assignments. + asset_kind: str + The kind of asset being validated, used in error messages (e.g. "branch" or "injection"). + + Returns + ------- + None + This function returns nothing and raises on invalid shapes. + + Raises + ------ + ValueError + If either switching table does not match the expected station dimensions. + """ + if asset_switching_table.shape != (busbar_count, asset_count): + raise ValueError( + f"{asset_kind}_switching_table shape {asset_switching_table.shape} does not match busbars " + f"{busbar_count} and {asset_kind} assets {asset_count}" + f" Station_id: {station_grid_model_id}, Name: {station_name}" + ) + + if asset_connectivity is not None and asset_connectivity.shape != (busbar_count, asset_count): + raise ValueError( + f"{asset_kind}_connectivity shape {asset_connectivity.shape} does not match busbars " + f"{busbar_count} and {asset_kind} assets {asset_count}" + f" Station_id: {station_grid_model_id}, Name: {station_name}" + ) + + +def _validate_station_physical_assignments( + station_grid_model_id: str, + station_name: Optional[str], + asset_switching_table: np.ndarray, + asset_connectivity: Optional[np.ndarray], + asset_kind: str, +) -> None: + """Validate that all current assignments are physically allowed. + + Parameters + ---------- + station_grid_model_id : str + Grid model id of the station being validated. + station_name : Optional[str] + Human-readable station name used in validation errors. + asset_switching_table : np.ndarray + Current station switching table. + asset_connectivity : Optional[np.ndarray] + Optional connectivity mask describing physically allowed assignments. + asset_kind: str + The kind of asset being validated, used in error messages (e.g. "branch" or "injection"). + + Returns + ------- + None + This function returns nothing and raises on invalid assignments. + + Raises + ------ + ValueError + If the switching table contains assignments forbidden by ``asset_connectivity``. + """ + if asset_connectivity is not None: + if np.logical_and(asset_switching_table, np.logical_not(asset_connectivity)).any(): + raise ValueError( + f"Not all current {asset_kind} assignments are physically allowed " + f"Station_id: {station_grid_model_id}, Name: {station_name}" + ) + + +class _StationStructure(BaseModel): + """Shared station fields and structural validators for station views.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + grid_model_id: str + """The unique identifier of the station. + + Expects the bus-branch model bus_id, not the full voltage level id. + + Included are all assets, busbars and couplers that are connectable via switches. + Buses in the same station that are connected via branches are excluded in this specific bus. + + This means, that two stations/buses can have the same elements if the station is currently split. + """ + + name: Optional[str] = None + """The name of the station.""" + + station_type: Optional[str] = None + """The type of the station.""" + + region: Optional[str] = None + """The region of the station.""" + + voltage_level: Optional[float] = None + """The voltage level of the station.""" + + busbars: list[Busbar] + """The list of busbars at the station.""" + + couplers: list[BusbarCoupler] + """The list of couplers at the station.""" + + branch_switching_table: StationSwitchingArray + """Holds the switching of each branch asset to each busbar, shape (n_bus, n_branch_asset). + + An entry is true if the asset is connected to the busbar. + Note: An asset can be connected to multiple busbars, in which case a closed coupler is assumed + to be present between these busbars. + Note: An asset can be connected to none of the busbars. In this case, the asset is intentionally + disconnected as part of a transmission line switching action. In practice, this usually involves + a separate switch from the asset-to-busbar couplers, as each asset usually has a switch that + completely disconnects it from the station. These switches are not modelled here, a + postprocessing routine needs to do the translation to this physical layout. Do not use + in_service for intentional disconnections. + """ + + injection_switching_table: StationSwitchingArray + """Holds the switching of each injection asset to each busbar, shape (n_bus, n_injection_asset).""" + + branch_connectivity: Optional[StationSwitchingArray] = None + """Holds all physically possible branch layouts, shape (n_bus, n_branch_asset).""" + + injection_connectivity: Optional[StationSwitchingArray] = None + """Holds all physically possible injection layouts, shape (n_bus, n_injection_asset).""" + + model_log: Optional[list[str]] = None + """Holds log messages from the model creation process. + + This can be used to store information about the model creation process, e.g. warnings or errors. + A potential use case is to inform the user about data quality issues e.g. missing the Asset Bay switches. + """ + + @field_validator( + "branch_switching_table", + "injection_switching_table", + "branch_connectivity", + "injection_connectivity", + mode="before", + ) + @classmethod + def normalize_station_tables(cls, v: Optional[Collection]) -> Optional[np.ndarray]: + """Normalize switching and connectivity table inputs to boolean arrays.""" + if v is None: + return None + return np.asarray(v, dtype=bool) + + @field_validator("busbars") + @classmethod + def check_int_id_unique(cls, v: list[Busbar]) -> list[Busbar]: + """Check if the int_ids of the busbars are unique.""" + int_ids = [busbar.int_id for busbar in v] + if len(int_ids) != len(set(int_ids)): + raise ValueError("busbar int_ids must be unique per station") + return v + + @field_validator("couplers") + @classmethod + def check_coupler_busbars_different(cls, v: list[BusbarCoupler]) -> list[BusbarCoupler]: + """Check if the couplers do not connect the same busbar on both ends.""" + for coupler in v: + if coupler.busbar_from_id == coupler.busbar_to_id: + raise ValueError(f"Coupler {coupler.grid_model_id} connects the same busbar on both ends") + return v + + @model_validator(mode="after") + def check_coupler_busbars_exist(self: "_StationStructure") -> "_StationStructure": + """Check if all coupler busbar ids exist in busbars.""" + busbar_ids = [busbar.int_id for busbar in self.busbars] + for coupler in self.couplers: + if coupler.busbar_from_id not in busbar_ids or coupler.busbar_to_id not in busbar_ids: + raise ValueError( + f"Coupler {coupler.grid_model_id} references non-existing busbars" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + return self + + @model_validator(mode="after") + def check_coupler_references(self: "_StationStructure") -> "_StationStructure": + """Check if all closed couplers reference in-service busbars.""" + busbar_state_map = {busbar.int_id: busbar.in_service for busbar in self.busbars} + for coupler in self.couplers: + if coupler.open or not coupler.in_service: + continue + if busbar_state_map[coupler.busbar_from_id] != busbar_state_map[coupler.busbar_to_id]: + raise ValueError( + f"Closed coupler {coupler.grid_model_id} connects out-of-service busbar with in-service busbar." + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + return self + + @model_validator(mode="after") + def check_bus_id(self: "_StationStructure") -> "_StationStructure": + """Check if station grid_model_id is in the busbar.bus_branch_bus_id.""" + busbar_grid_model_id = [busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id is not None] + if len(busbar_grid_model_id) > 0 and self.grid_model_id not in busbar_grid_model_id: + raise ValueError( + f"Station grid_model_id {self.grid_model_id} does not exist in busbars bus_branch_bus_id" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + + return self + + def is_split(self) -> bool: + """Return whether the station view spans more than one non-empty bus-branch bus id.""" + bus_ids = {busbar.bus_branch_bus_id for busbar in self.busbars if busbar.bus_branch_bus_id not in {None, ""}} + return len(bus_ids) > 1 + + +class RawStation(_StationStructure): + """Station data stored inside a topology without embedded asset payloads. + + The station identity still refers to a bus-branch model bus_id for one splitable station view. + Asset membership is expressed through the aligned station-local arrays instead of embedded + SwitchableAsset payloads. + """ + + branch_connections: list[StationAssetConnection] = Field(default_factory=list) + """Station-local branch references aligned with ``branch_switching_table``.""" + + injection_connections: list[StationAssetConnection] = Field(default_factory=list) + """Station-local injection references aligned with ``injection_switching_table``.""" + + def with_asset_terminals(self, asset_terminals: list[Optional[BranchEnd]]) -> "RawStation": + """Return a copy with updated branch terminals.""" + if len(asset_terminals) != len(self.branch_connections): + raise ValueError( + f"asset_terminals length {len(asset_terminals)} does not match branch_connections length " + f"{len(self.branch_connections)}" + f" Station_id: {self.grid_model_id}, Name: {self.name}" + ) + + return self.model_copy( + update={ + "branch_connections": [ + asset_connection.model_copy(update={"terminal": asset_terminal}) + for asset_connection, asset_terminal in zip(self.branch_connections, asset_terminals, strict=True) + ] + } + ) + + @model_validator(mode="after") + def check_asset_reference_alignment(self: "RawStation") -> "RawStation": + """Check if station-local asset reference arrays are aligned.""" + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.branch_connections), + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.branch_switching_table, + asset_connectivity=self.branch_connectivity, + asset_kind="branch", + ) + _validate_station_switching_tables( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + busbar_count=len(self.busbars), + asset_count=len(self.injection_connections), + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", + ) + _validate_station_physical_assignments( + station_grid_model_id=self.grid_model_id, + station_name=self.name, + asset_switching_table=self.injection_switching_table, + asset_connectivity=self.injection_connectivity, + asset_kind="injection", + ) + return self + + def __eq__(self, other: object) -> bool: + """Check if two topology stations are equal.""" + if not isinstance(other, RawStation): + return False + return ( + self.grid_model_id == other.grid_model_id + and self.name == other.name + and self.station_type == other.station_type + and self.region == other.region + and self.voltage_level == other.voltage_level + and self.busbars == other.busbars + and self.couplers == other.couplers + and self.branch_connections == other.branch_connections + and self.injection_connections == other.injection_connections + and np.array_equal(self.branch_switching_table, other.branch_switching_table) + and np.array_equal(self.injection_switching_table, other.injection_switching_table) + and ( + np.array_equal(self.branch_connectivity, other.branch_connectivity) + if (self.branch_connectivity is not None and other.branch_connectivity is not None) + else self.branch_connectivity == other.branch_connectivity + ) + and ( + np.array_equal(self.injection_connectivity, other.injection_connectivity) + if (self.injection_connectivity is not None and other.injection_connectivity is not None) + else self.injection_connectivity == other.injection_connectivity + ) + and self.model_log == other.model_log + ) + + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "RawStation": + """Copy and revalidate the station.""" + payload = _merged_round_trip_payload(self, update, deep=deep) + return type(self).model_validate(payload) + + def get_connected_assets( + self, + busbar_index: int, + topology_assets: Optional[list[SwitchableAsset]] = None, + asset_scope: Literal["all", "branch", "injection"] = "all", + ) -> list[SwitchableAsset]: + """Return in-service topology assets connected to one busbar. + + Parameters + ---------- + busbar_index : int + Row index into the station switching tables. + topology_assets : Optional[list[SwitchableAsset]] + Topology-owned assets used to resolve raw station asset references. + asset_scope : Literal["all", "branch", "injection"] + Restrict the lookup to branch or injection connections. + + Returns + ------- + list[SwitchableAsset] + Connected in-service assets for the requested busbar and scope. + + Raises + ------ + ValueError + If topology assets are missing for a raw station lookup. + """ + if topology_assets is None: + raise ValueError("topology_assets must be provided when resolving connected assets for a RawStation") + + asset_map = {asset.grid_model_id: asset for asset in topology_assets} + if asset_scope == "branch": + return [ + asset_map[asset_connection.asset_id] + for asset_connection, is_connected in zip( + self.branch_connections, + self.branch_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_map[asset_connection.asset_id].in_service + ] + if asset_scope == "injection": + return [ + asset_map[asset_connection.asset_id] + for asset_connection, is_connected in zip( + self.injection_connections, + self.injection_switching_table[busbar_index], + strict=True, + ) + if is_connected and asset_map[asset_connection.asset_id].in_service + ] + return [ + *self.get_connected_assets(busbar_index, topology_assets=topology_assets, asset_scope="branch"), + *self.get_connected_assets(busbar_index, topology_assets=topology_assets, asset_scope="injection"), + ] diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py new file mode 100644 index 000000000..c4cb5d29f --- /dev/null +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py @@ -0,0 +1,144 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Conversions between materialized stations and canonical topology models.""" + +from beartype.typing import Optional +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.assets import AssetBay, SwitchableAsset, normalize_switchable_asset_payload +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import RawStation, StationAssetConnection + + +def topology_parts_from_materialized_station( + station: MaterializedStation, +) -> tuple[RawStation, list[SwitchableAsset], list[AssetBay]]: + """Extract topology-owned payloads from a materialized station. + + Parameters + ---------- + station : MaterializedStation + Materialized station to decompose. + + Returns + ------- + tuple[RawStation, list[SwitchableAsset], list[AssetBay]] + Raw station record plus topology-owned assets and asset bays derived from the station. + """ + branch_assets: list[SwitchableAsset] = [] + injection_assets: list[SwitchableAsset] = [] + asset_bays: list[AssetBay] = [] + branch_connections: list[StationAssetConnection] = [] + injection_connections: list[StationAssetConnection] = [] + for asset_connection in station.branch_connections: + asset = normalize_switchable_asset_payload(asset_connection.asset.model_dump(round_trip=True)) + asset_bay = asset_connection.asset_bay + asset_id = asset.grid_model_id + asset_bay_id: Optional[str] = None + + if asset_bay is not None: + asset_bay_id = asset_bay.asset_bay_id + asset_bays.append(asset_bay.model_copy(deep=True)) + + branch_assets.append(asset.model_copy(deep=True)) + branch_connections.append( + StationAssetConnection( + asset_id=asset_id, + terminal=asset_connection.terminal, + asset_bay_id=asset_bay_id, + ) + ) + + for asset_connection in station.injection_connections: + asset = normalize_switchable_asset_payload(asset_connection.asset.model_dump(round_trip=True)) + asset_bay = asset_connection.asset_bay + asset_id = asset.grid_model_id + asset_bay_id = asset_bay.asset_bay_id if asset_bay is not None else None + + if asset_bay is not None: + asset_bays.append(asset_bay.model_copy(deep=True)) + + injection_assets.append(asset.model_copy(deep=True)) + injection_connections.append( + StationAssetConnection( + asset_id=asset_id, + terminal=asset_connection.terminal, + asset_bay_id=asset_bay_id, + ) + ) + + return ( + RawStation( + grid_model_id=station.grid_model_id, + name=station.name, + station_type=station.station_type, + region=station.region, + voltage_level=station.voltage_level, + busbars=station.busbars, + couplers=station.couplers, + branch_connections=branch_connections, + injection_connections=injection_connections, + branch_switching_table=station.branch_switching_table, + injection_switching_table=station.injection_switching_table, + branch_connectivity=station.branch_connectivity, + injection_connectivity=station.injection_connectivity, + model_log=station.model_log, + ), + [*branch_assets, *injection_assets], + asset_bays, + ) + + +def topology_from_materialized_stations(reference_topology: Topology, stations: list[MaterializedStation]) -> Topology: + """Create a topology from materialized stations. + + Parameters + ---------- + reference_topology : Topology + Reference topology providing shared metadata. + stations : list[MaterializedStation] + Materialized stations to convert. + + Returns + ------- + Topology + Topology containing raw stations and topology-owned payloads derived from ``stations``. + """ + topology_stations: list[RawStation] = [] + topology_branch_assets: dict[str, SwitchableAsset] = {} + topology_injection_assets: dict[str, SwitchableAsset] = {} + topology_asset_bays: dict[str, AssetBay] = {} + + for station in stations: + topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_stations.append(topology_station) + for asset in station_assets: + topology_assets = topology_branch_assets if asset.is_branch() is not False else topology_injection_assets + existing_asset = topology_assets.get(asset.grid_model_id) + if existing_asset is None: + topology_assets[asset.grid_model_id] = asset + elif existing_asset != asset: + raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") + for asset_bay in station_asset_bays: + existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) + if existing_asset_bay is None: + topology_asset_bays[asset_bay.asset_bay_id] = asset_bay + elif existing_asset_bay != asset_bay: + raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + + return Topology( + topology_id=reference_topology.topology_id, + grid_model_file=reference_topology.grid_model_file, + name=reference_topology.name, + raw_stations=topology_stations, + branch_assets=list(topology_branch_assets.values()), + injection_assets=list(topology_injection_assets.values()), + asset_bays=list(topology_asset_bays.values()), + asset_setpoints=reference_topology.asset_setpoints, + timestamp=reference_topology.timestamp, + metrics=reference_topology.metrics, + ) diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/backend.py b/packages/interfaces_pkg/src/toop_engine_interfaces/backend.py index e88c9b5d9..2df5a5921 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/backend.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/backend.py @@ -12,7 +12,7 @@ import numpy as np from beartype.typing import Optional, Sequence, Union from jaxtyping import Bool, Float, Int -from toop_engine_interfaces.asset_topology import Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology class BackendInterface(ABC): diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/messages/lf_service/loadflow_commands.py b/packages/interfaces_pkg/src/toop_engine_interfaces/messages/lf_service/loadflow_commands.py index a28d2ae5d..2a9e31ac3 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/messages/lf_service/loadflow_commands.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/messages/lf_service/loadflow_commands.py @@ -25,7 +25,7 @@ from beartype.typing import Literal, Optional, Protocol, Union from pydantic import BaseModel, Field -from toop_engine_interfaces.asset_topology import Strategy +from toop_engine_interfaces.asset_topology.asset_topology import Strategy from toop_engine_interfaces.loadflow_results import LoadflowResults from toop_engine_interfaces.nminus1_definition import GridElement, Nminus1Definition diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py b/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py index 3f37a1aa3..adb42e2dc 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/stored_action_set.py @@ -43,7 +43,8 @@ from fsspec.implementations.local import LocalFileSystem from jaxtyping import Bool from pydantic import BaseModel, ConfigDict, model_validator -from toop_engine_interfaces.asset_topology import MaterializedStation, Topology +from toop_engine_interfaces.asset_topology.asset_topology import Topology +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.filesystem_helper import save_pydantic_model_fs from toop_engine_interfaces.nminus1_definition import GridElement diff --git a/packages/interfaces_pkg/tests/messages/lf_service/test_loadflow_commands.py b/packages/interfaces_pkg/tests/messages/lf_service/test_loadflow_commands.py index ede3e0c6e..3a48ea4ee 100644 --- a/packages/interfaces_pkg/tests/messages/lf_service/test_loadflow_commands.py +++ b/packages/interfaces_pkg/tests/messages/lf_service/test_loadflow_commands.py @@ -5,7 +5,7 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 -from toop_engine_interfaces.asset_topology import Strategy +from toop_engine_interfaces.asset_topology.asset_topology import Strategy from toop_engine_interfaces.messages.lf_service import loadflow_commands as lf_cmds from toop_engine_interfaces.nminus1_definition import GridElement, Nminus1Definition diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index c47ab6bdd..4c0f0a1ae 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -12,23 +12,11 @@ import numpy as np import pytest from pydantic import ValidationError -from toop_engine_interfaces.asset_topology import ( - AssetBay, - BranchAsset, - Busbar, - BusbarCoupler, - InjectionAsset, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - StationAssetConnection, - SwitchableAsset, Topology, - build_asset_bay_id, - normalize_switchable_asset_payload, - topology_from_materialized_stations, ) -from toop_engine_interfaces.asset_topology_helpers import ( +from toop_engine_interfaces.asset_topology.asset_topology_helpers import ( filter_assets_by_type, filter_disconnected_busbars, filter_duplicate_couplers, @@ -38,6 +26,19 @@ load_asset_topology, save_asset_topology, ) +from toop_engine_interfaces.asset_topology.assets import ( + AssetBay, + BranchAsset, + Busbar, + BusbarCoupler, + InjectionAsset, + SwitchableAsset, + build_asset_bay_id, + normalize_switchable_asset_payload, +) +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection +from toop_engine_interfaces.asset_topology.topology_conversion import topology_from_materialized_stations def materialized_asset_connections( diff --git a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py index 2f591eb30..fa813d19a 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py +++ b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py @@ -9,19 +9,11 @@ import numpy as np import pytest -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - BusbarCoupler, - InjectionAsset, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - StationAssetConnection, - SwitchableAsset, Topology, ) -from toop_engine_interfaces.asset_topology_helpers import ( +from toop_engine_interfaces.asset_topology.asset_topology_helpers import ( filter_disconnected_busbars, filter_duplicate_couplers, filter_out_of_service, @@ -36,6 +28,9 @@ station_diff, topology_diff, ) +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, InjectionAsset, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection def build_raw_station( diff --git a/packages/interfaces_pkg/tests/test_filesystem_helpers.py b/packages/interfaces_pkg/tests/test_filesystem_helpers.py index e3c4d08d3..88827285c 100644 --- a/packages/interfaces_pkg/tests/test_filesystem_helpers.py +++ b/packages/interfaces_pkg/tests/test_filesystem_helpers.py @@ -10,7 +10,7 @@ import numpy as np import pytest from fsspec.implementations.dirfs import DirFileSystem -from toop_engine_interfaces.asset_topology import Busbar +from toop_engine_interfaces.asset_topology.assets import Busbar from toop_engine_interfaces.filesystem_helper import ( copy_file_fs, load_numpy_filesystem, diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index 131d83320..71183dd18 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -12,18 +12,13 @@ import pytest from fsspec.implementations.dirfs import DirFileSystem from pydantic import ValidationError -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - BusbarCoupler, - InjectionAsset, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - StationAssetConnection, - SwitchableAsset, Topology, ) +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, InjectionAsset, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection from toop_engine_interfaces.stored_action_set import ( ActionSet, StationDiffArray, diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index 1c8171619..09352bf7a 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -12,17 +12,13 @@ import numpy as np import pytest from fsspec.implementations.local import LocalFileSystem -from toop_engine_interfaces.asset_topology import ( - BranchAsset, - Busbar, - BusbarCoupler, - MaterializedAssetConnection, - MaterializedStation, +from toop_engine_interfaces.asset_topology.asset_topology import ( RawStation, - StationAssetConnection, - SwitchableAsset, Topology, ) +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection from toop_engine_interfaces.filesystem_helper import save_pydantic_model_fs from toop_engine_interfaces.stored_action_set import ActionSet, load_action_set, load_action_set_fs, save_action_set 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 c8e7ea7d7..aa15746f2 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 @@ -18,7 +18,7 @@ from beartype.typing import Collection, Optional from toop_engine_contingency_analysis.ac_loadflow_service.compute_metrics import compute_metrics as compute_metrics_lfs from toop_engine_dc_solver.postprocess.abstract_runner import AbstractLoadflowRunner, AdditionalActionInfo -from toop_engine_interfaces.asset_topology import RealizedTopology +from toop_engine_interfaces.asset_topology.applied_topology import RealizedTopology from toop_engine_interfaces.loadflow_result_helpers_polars import ( concatenate_loadflow_results_polars, subset_contingencies_polars, From bfe99a40bd2e88332166db4c4a9ae4a3f5c46544 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 22 Jun 2026 15:47:24 +0200 Subject: [PATCH 18/24] fix import tests Signed-off-by: Leonard Hilfrich --- .../src/toop_engine_dc_solver/preprocess/network_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 44877ea0c..2b0a1271b 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 @@ -739,13 +739,13 @@ def extract_nminus1_definition(network_data: NetworkData) -> Nminus1Definition: network_data.simplified_asset_topology if network_data.simplified_asset_topology else network_data.asset_topology ) monitored_nodes = [ - MonitoredElement(id=busbar.grid_model_id, name=busbar.name or "", type=busbar.type, kind="bus") + MonitoredElement(id=busbar.grid_model_id, name=busbar.name or "", type=busbar.busbar_type, kind="bus") for station in asset_topology.materialize_stations() for busbar in station.busbars ] monitored_switches = [ - MonitoredElement(id=switch.grid_model_id, name=switch.name or "", type=switch.type, kind="switch") + MonitoredElement(id=switch.grid_model_id, name=switch.name or "", type=switch.coupler_type, kind="switch") for station in asset_topology.materialize_stations() for switch in station.couplers ] From f95b23a30d56667ccda268a21d4e657804df4205 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Mon, 22 Jun 2026 17:15:47 +0200 Subject: [PATCH 19/24] fix issues Signed-off-by: Leonard Hilfrich --- docs/interfaces/asset_topology.md | 6 +- docs/references/reference_interfaces.md | 2 +- .../postprocess/realize_assignment.py | 86 ++++++++++++++++--- .../dc_solver_pkg/tests/export/test_export.py | 10 +-- .../asset_topology/__init__.py | 38 ++++++++ .../asset_topology/station_models.py | 7 +- 6 files changed, 125 insertions(+), 24 deletions(-) diff --git a/docs/interfaces/asset_topology.md b/docs/interfaces/asset_topology.md index e01f89445..8f8f7ef7d 100644 --- a/docs/interfaces/asset_topology.md +++ b/docs/interfaces/asset_topology.md @@ -10,16 +10,16 @@ Asset Topology is essential when stations do not allow free assignment of lines Collection of time steps, each represented by a [`Topology`][toop_engine_interfaces.asset_topology.Topology]. - [`Topology`][toop_engine_interfaces.asset_topology.Topology] - Stores lean [`RawStation`][toop_engine_interfaces.asset_topology.RawStation] records in `raw_stations`, topology-owned canonical assets in `assets`, and optional [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] objects. Rich [`MaterializedStation`][toop_engine_interfaces.asset_topology.MaterializedStation] objects are reconstructed with [`Topology.materialize_stations()`][toop_engine_interfaces.asset_topology.Topology.materialize_stations]. + Stores lean [`RawStation`][toop_engine_interfaces.asset_topology.RawStation] records in `raw_stations`, topology-owned canonical branch and injection assets in `branch_assets` and `injection_assets`, and optional [`AssetSetpoint`][toop_engine_interfaces.asset_topology.AssetSetpoint] objects. Rich [`MaterializedStation`][toop_engine_interfaces.asset_topology.MaterializedStation] objects are reconstructed with [`Topology.materialize_stations()`][toop_engine_interfaces.asset_topology.Topology.materialize_stations]. - [`MaterializedStation`][toop_engine_interfaces.asset_topology.MaterializedStation] Contains lists of [`Busbar`][toop_engine_interfaces.asset_topology.Busbar], [`BusbarCoupler`][toop_engine_interfaces.asset_topology.BusbarCoupler], and [`SwitchableAsset`][toop_engine_interfaces.asset_topology.SwitchableAsset]. - Includes `asset_switching_table`, the current switch connection layout and `asset_connectivity`, all possible selections. + Includes `branch_switching_table` and `injection_switching_table` for the current switch connection layout, plus `branch_connectivity` and `injection_connectivity` for physically allowed selections. The `grid_model_id` refers to the bus-branch bus id of the splitable station view, not to the full voltage level id. - [`RawStation`][toop_engine_interfaces.asset_topology.RawStation] Stores the lean station representation used inside [`Topology`][toop_engine_interfaces.asset_topology.Topology]. - Instead of embedded asset payloads it stores aligned station-local arrays `asset_ids`, `asset_branch_ends`, and `asset_bay_ids`. + Instead of embedded asset payloads it stores aligned station-local `branch_connections` and `injection_connections` arrays referencing topology-owned assets and asset bays. - [`Busbar`][toop_engine_interfaces.asset_topology.Busbar] Represents a single busbar in a station. diff --git a/docs/references/reference_interfaces.md b/docs/references/reference_interfaces.md index 74dcf3f09..1bbff5037 100644 --- a/docs/references/reference_interfaces.md +++ b/docs/references/reference_interfaces.md @@ -26,4 +26,4 @@ ::: toop_engine_interfaces.asset_topology ## Asset Topology Helper -::: toop_engine_interfaces.asset_topology_helpers +::: toop_engine_interfaces.asset_topology.asset_topology_helpers diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py index d2eed3855..2a1834f43 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/realize_assignment.py @@ -18,11 +18,50 @@ from toop_engine_dc_solver.preprocess.helpers.switching_distance import per_station_switching_distance from toop_engine_dc_solver.preprocess.preprocess_switching import OptimalSeparationSetInfo from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.station_models import ( + _validate_station_physical_assignments, + _validate_station_switching_tables, +) from toop_engine_interfaces.messages.preprocess.preprocess_commands import ReassignmentLimits logger = structlog.get_logger(__name__) +def _validate_realized_station_update( + station: MaterializedStation, + action_switching: Bool[np.ndarray, " n_busbars n_branches"], + action_coupler_states: Bool[np.ndarray, " n_couplers"], +) -> None: + """Validate the mutated fields of a realized station. + + The realization step only changes the branch switching table and coupler open states. + All other station-local payloads stay identical to the validated input station, so a full + pydantic round-trip per action is unnecessary in this hot path. + """ + if len(action_coupler_states) != len(station.couplers): + raise ValueError( + f"Coupler state count {len(action_coupler_states)} does not match station coupler count " + f"{len(station.couplers)} for station {station.grid_model_id}." + ) + + _validate_station_switching_tables( + station_grid_model_id=station.grid_model_id, + station_name=station.name, + busbar_count=len(station.busbars), + asset_count=len(station.branch_connections), + asset_switching_table=np.asarray(action_switching, dtype=bool), + asset_connectivity=station.branch_connectivity, + asset_kind="branch", + ) + _validate_station_physical_assignments( + station_grid_model_id=station.grid_model_id, + station_name=station.name, + asset_switching_table=np.asarray(action_switching, dtype=bool), + asset_connectivity=station.branch_connectivity, + asset_kind="branch", + ) + + @partial(jax.jit, static_argnames=("batch_size", "choice_heuristic")) def _compute_switching_tables_for_actions( local_branch_action_set: Bool[Array, " n_combinations n_assets"], @@ -482,21 +521,42 @@ def realise_ba_to_physical_topo_per_station_jax( phy_reassignment_distance = phy_reassignment_distance[within_limit] # Create the realised stations - realised_stations = [ - station.model_copy( - update={ - "branch_switching_table": action_switching, - "couplers": [ - coupler.model_copy(update={"open": bool(open)}) - for coupler, open in zip(station.couplers, action_coupler_states, strict=True) + coupler_payloads = [coupler.model_dump(round_trip=True) for coupler in station.couplers] + realised_stations: list[MaterializedStation] = [] + for action_switching, action_coupler_states in zip(switching_table, chosen_coupler_state, strict=True): + if validate: + _validate_realized_station_update( + station=station, + action_switching=action_switching, + action_coupler_states=action_coupler_states, + ) + + realised_stations.append( + MaterializedStation.model_construct( + grid_model_id=station.grid_model_id, + name=station.name, + station_type=station.station_type, + region=station.region, + voltage_level=station.voltage_level, + busbars=station.busbars, + couplers=[ + type(coupler).model_construct(**(payload | {"open": bool(open_state)})) + for coupler, payload, open_state in zip( + station.couplers, + coupler_payloads, + action_coupler_states, + strict=True, + ) ], - } + branch_connections=station.branch_connections, + injection_connections=station.injection_connections, + branch_switching_table=action_switching, + injection_switching_table=station.injection_switching_table, + branch_connectivity=station.branch_connectivity, + injection_connectivity=station.injection_connectivity, + model_log=station.model_log, + ) ) - for action_switching, action_coupler_states in zip(switching_table, chosen_coupler_state, strict=True) - ] - if validate: - for realised_station in realised_stations: - MaterializedStation.model_validate(realised_station) # Convert the busbar mapping to a list of busbar A mappings busbar_mappings_converted = [np.flatnonzero(~mapping).tolist() for mapping in chosen_busbar_mapping] diff --git a/packages/dc_solver_pkg/tests/export/test_export.py b/packages/dc_solver_pkg/tests/export/test_export.py index be5d91cda..fe3627070 100644 --- a/packages/dc_solver_pkg/tests/export/test_export.py +++ b/packages/dc_solver_pkg/tests/export/test_export.py @@ -60,20 +60,20 @@ def test_get_changing_switches_from_actions_matches_network_diff( target_station = basic_node_breaker_topology.materialize_stations()[0] changed_station = target_station.model_copy( update={ - "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) target_raw_station = basic_node_breaker_topology.raw_stations[0] starting_station = target_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_station.couplers], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) starting_raw_station = target_raw_station.model_copy( update={ "couplers": [coupler.model_copy(update={"open": False}) for coupler in target_raw_station.couplers], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) target_topology = build_test_topology(basic_node_breaker_topology, [target_raw_station]) @@ -98,7 +98,7 @@ def test_get_changing_switches_from_action_set_matches_expanded_inputs( target_station = basic_node_breaker_topology.materialize_stations()[0] changed_station = target_station.model_copy( update={ - "asset_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), + "branch_switching_table": np.array([[False, False, True], [True, True, False]], dtype=bool), } ) @@ -108,7 +108,7 @@ def test_get_changing_switches_from_action_set_matches_expanded_inputs( coupler.model_copy(update={"open": False}) for coupler in basic_node_breaker_topology.raw_stations[0].couplers ], - "asset_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), + "branch_switching_table": np.array([[True, False, True], [False, True, False]], dtype=bool), } ) starting_topology = build_test_topology(basic_node_breaker_topology, [starting_raw_station]) diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py index e69de29bb..83ac23da5 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/__init__.py @@ -0,0 +1,38 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Public asset topology API. + +This package re-exports the main topology data models so downstream imports and +documentation can refer to a stable package-level namespace. +""" + +from toop_engine_interfaces.asset_topology.applied_topology import AppliedStation, RealizedTopology +from toop_engine_interfaces.asset_topology.asset_topology import CircuitGroup, Strategy, Topology +from toop_engine_interfaces.asset_topology.assets import AssetBay, AssetSetpoint, Busbar, BusbarCoupler, SwitchableAsset +from toop_engine_interfaces.asset_topology.materialized_topology import ( + MaterializedAssetConnection, + MaterializedStation, +) +from toop_engine_interfaces.asset_topology.station_models import RawStation, StationAssetConnection + +__all__ = [ + "AppliedStation", + "AssetBay", + "AssetSetpoint", + "Busbar", + "BusbarCoupler", + "CircuitGroup", + "MaterializedAssetConnection", + "MaterializedStation", + "RawStation", + "RealizedTopology", + "StationAssetConnection", + "Strategy", + "SwitchableAsset", + "Topology", +] diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py index aaf08d402..993cdfeed 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py @@ -10,7 +10,7 @@ from copy import deepcopy import numpy as np -from beartype.typing import Any, Collection, Literal, Optional, TypeAlias +from beartype.typing import Any, Literal, Optional, TypeAlias from numpydantic import NDArray, Shape from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from toop_engine_interfaces.asset_topology.asset_types import BranchEnd @@ -207,7 +207,10 @@ class _StationStructure(BaseModel): mode="before", ) @classmethod - def normalize_station_tables(cls, v: Optional[Collection]) -> Optional[np.ndarray]: + def normalize_station_tables( + cls, + v: Optional[Any], # noqa: ANN401 + ) -> Optional[np.ndarray]: """Normalize switching and connectivity table inputs to boolean arrays.""" if v is None: return None From a2cbd1b389402e6e8fd2d645d73efdf0d0b267fa Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Tue, 23 Jun 2026 11:53:59 +0200 Subject: [PATCH 20/24] fix: remove is branch, add circuit groups Signed-off-by: Leonard Hilfrich --- .../tests/jax/test_busbar_outage.py | 51 ++++++----- .../powsybl/powsybl_asset_topo.py | 8 +- .../network_graph/powsybl_station_to_graph.py | 21 +++-- .../pandapower_import/asset_topology.py | 22 +++-- .../asset_topology/asset_topology.py | 26 ++++-- .../asset_topology/assets.py | 22 ----- .../asset_topology/topology_conversion.py | 54 +++++------- .../tests/test_asset_topology.py | 87 ++++++++++++++++++- 8 files changed, 187 insertions(+), 104 deletions(-) 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 7d52adae1..1a31ddc31 100644 --- a/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py +++ b/packages/dc_solver_pkg/tests/jax/test_busbar_outage.py @@ -104,8 +104,8 @@ def test_perform_outage_single_busbar( for station in network_data_preprocessed.asset_topology.materialize_stations(): if station_id == station.grid_model_id: for index, busbar in enumerate(station.busbars): - connected_assets = station.get_connected_assets(index) - connected_branches = [asset.grid_model_id for asset in connected_assets if asset.is_branch()] + connected_branch_assets = station.get_connected_assets(index, asset_scope="branch") + connected_branches = [asset.grid_model_id for asset in connected_branch_assets] connected_branches = [ network_data_preprocessed.branch_ids.index(branch_id) for branch_id in connected_branches ] @@ -200,8 +200,8 @@ def test_perform_outage_single_busbar_with_disconnections( for station in network_data_preprocessed.asset_topology.materialize_stations(): if station_id == station.grid_model_id: for index, busbar in enumerate(station.busbars): - connected_assets = station.get_connected_assets(index) - connected_branches = [asset.grid_model_id for asset in connected_assets if asset.is_branch()] + connected_branch_assets = station.get_connected_assets(index, asset_scope="branch") + connected_branches = [asset.grid_model_id for asset in connected_branch_assets] connected_branches = [ network_data_preprocessed.branch_ids.index(branch_id) for branch_id in connected_branches ] @@ -344,16 +344,17 @@ def get_busbar_injection_map( topology = network.simplified_asset_topology or network.asset_topology topology_assets = [*topology.branch_assets, *topology.injection_assets] for i, bb in enumerate(station.busbars): - connected_assets = station.get_connected_assets(i, topology_assets=topology_assets) + connected_injection_assets = station.get_connected_assets( + i, topology_assets=topology_assets, asset_scope="injection" + ) injection: Float[np.ndarray, " n_timestep"] = np.zeros(network.nodal_injection.shape[0], float) - for asset in connected_assets: - if not asset.is_branch() and asset.in_service: - if asset.grid_model_id not in network.injection_ids: - logger.warning(f"Asset {asset.grid_model_id} is not a valid injection. Might have been removed.") - continue - injection_index = network.injection_ids.index(asset.grid_model_id) - injection += network.mw_injections[:, injection_index] + for asset in connected_injection_assets: + if asset.grid_model_id not in network.injection_ids: + logger.warning(f"Asset {asset.grid_model_id} is not a valid injection. Might have been removed.") + continue + injection_index = network.injection_ids.index(asset.grid_model_id) + injection += network.mw_injections[:, injection_index] busbar_injection_map[bb.grid_model_id] = injection return busbar_injection_map @@ -506,9 +507,10 @@ def test_compare_loadflows_non_rel_bb_outage_powsybl( continue bb_index = get_busbar_index(station, bb.grid_model_id) connected_assets = station.get_connected_assets(bb_index) + connected_branch_assets = station.get_connected_assets(bb_index, asset_scope="branch") connected_asset_ids = [asset.grid_model_id for asset in connected_assets] connected_branch_indices = [ - network_data.branch_ids.index(asset.grid_model_id) for asset in connected_assets if asset.is_branch() + network_data.branch_ids.index(asset.grid_model_id) for asset in connected_branch_assets ] copy_net.remove_elements(connected_asset_ids) config = pp.loadflow.Parameters( @@ -518,11 +520,10 @@ def test_compare_loadflows_non_rel_bb_outage_powsybl( # Get the stub branches connected to this busbar stub_branch_indices = [] - for asset in connected_assets: - if asset.is_branch(): - br_index = network_data.branch_ids.index(asset.grid_model_id) - if network_data.bridging_branch_mask[br_index]: - stub_branch_indices.append(br_index) + for asset in connected_branch_assets: + br_index = network_data.branch_ids.index(asset.grid_model_id) + if network_data.bridging_branch_mask[br_index]: + stub_branch_indices.append(br_index) # All the loadflows should match except the stub branch and the disconnected branches where the load flow should be zero. # The stub branch lf will not match @@ -596,11 +597,10 @@ def test_compare_loadflows_rel_bb_outage( copy_net = copy.deepcopy(net) bb_index = get_busbar_index(modified_station, bb.grid_model_id) connected_assets = modified_station.get_connected_assets(bb_index) + connected_branch_assets = modified_station.get_connected_assets(bb_index, asset_scope="branch") connected_asset_ids = [asset.grid_model_id for asset in connected_assets] connected_branch_indices = [ - network_data_test_grid.branch_ids.index(asset.grid_model_id) - for asset in connected_assets - if asset.is_branch() + network_data_test_grid.branch_ids.index(asset.grid_model_id) for asset in connected_branch_assets ] copy_net.remove_elements(connected_asset_ids) config = pp.loadflow.Parameters( @@ -610,11 +610,10 @@ def test_compare_loadflows_rel_bb_outage( # Get the stub branches connected to this busbar stub_branch_indices = [] - for asset in connected_assets: - if asset.is_branch(): - br_index = network_data_test_grid.branch_ids.index(asset.grid_model_id) - if network_data_test_grid.bridging_branch_mask[br_index]: - stub_branch_indices.append(br_index) + for asset in connected_branch_assets: + br_index = network_data_test_grid.branch_ids.index(asset.grid_model_id) + if network_data_test_grid.bridging_branch_mask[br_index]: + stub_branch_indices.append(br_index) # The stub branch and skeleton branch lf may not match. Also, the loadflow in skeleton branch and stub branch # will not neccessarily be 0 diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index f2cda1de3..b16fe66d2 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -250,7 +250,9 @@ def _get_branch_station_assets_from_df( normalize_switchable_asset_payload(switchable_asset) for switchable_asset in station_elements.to_dict(orient="records") ] - branch_mask = np.asarray([asset.is_branch() is not False for asset in normalized_assets], dtype=bool) + if any(not isinstance(asset, (BranchAsset, InjectionAsset)) for asset in normalized_assets): + raise ValueError("All station assets must normalize to BranchAsset or InjectionAsset") + branch_mask = np.asarray([isinstance(asset, BranchAsset) for asset in normalized_assets], dtype=bool) branch_assets = [ asset if isinstance(asset, BranchAsset) else BranchAsset.model_validate(asset.model_dump()) for asset, is_branch in zip(normalized_assets, branch_mask, strict=True) @@ -297,7 +299,9 @@ def _get_injection_station_assets_from_df( normalize_switchable_asset_payload(switchable_asset) for switchable_asset in station_elements.to_dict(orient="records") ] - injection_mask = np.asarray([asset.is_branch() is False for asset in normalized_assets], dtype=bool) + if any(not isinstance(asset, (BranchAsset, InjectionAsset)) for asset in normalized_assets): + raise ValueError("All station assets must normalize to BranchAsset or InjectionAsset") + injection_mask = np.asarray([isinstance(asset, InjectionAsset) for asset in normalized_assets], dtype=bool) injection_assets = [ asset if isinstance(asset, InjectionAsset) else InjectionAsset.model_validate(asset.model_dump()) for asset, is_injection in zip(normalized_assets, injection_mask, strict=True) 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 5b71bf43f..5bdcd9f3e 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 @@ -54,7 +54,12 @@ from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, ) -from toop_engine_interfaces.asset_topology.assets import AssetBay, normalize_switchable_asset_payload +from toop_engine_interfaces.asset_topology.assets import ( + AssetBay, + BranchAsset, + InjectionAsset, + normalize_switchable_asset_payload, +) from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.asset_topology.topology_conversion import topology_parts_from_materialized_station from toop_engine_interfaces.messages.preprocess.preprocess_commands import CgmesImporterParameters @@ -228,7 +233,9 @@ def _get_station_asset_connections( ] remove_suffix_from_switchable_assets(assets) - branch_mask = [asset.is_branch() is not False for asset in assets] + branch_mask = [isinstance(asset, BranchAsset) for asset in assets] + if any(not isinstance(asset, (BranchAsset, InjectionAsset)) for asset in assets): + raise ValueError("All station assets must normalize to BranchAsset or InjectionAsset") branch_connections: list[MaterializedAssetConnection] = [] injection_connections: list[MaterializedAssetConnection] = [] for asset, is_branch in zip(assets, branch_mask, strict=True): @@ -641,9 +648,11 @@ def get_topology(network: Network, network_masks: NetworkMasks, importer_paramet topology_assets = {} topology_asset_bays = {} for station in station_list: - topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_station, station_branch_assets, station_injection_assets, station_asset_bays = ( + topology_parts_from_materialized_station(station) + ) topology_stations.append(topology_station) - for asset in station_assets: + for asset in [*station_branch_assets, *station_injection_assets]: existing_asset = topology_assets.get(asset.grid_model_id) if existing_asset is None: topology_assets[asset.grid_model_id] = asset @@ -659,8 +668,8 @@ def get_topology(network: Network, network_masks: NetworkMasks, importer_paramet raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") topology_assets_list = list(topology_assets.values()) - branch_assets = [asset for asset in topology_assets_list if asset.is_branch() is not False] - injection_assets = [asset for asset in topology_assets_list if asset.is_branch() is False] + branch_assets = [asset for asset in topology_assets_list if isinstance(asset, BranchAsset)] + injection_assets = [asset for asset in topology_assets_list if isinstance(asset, InjectionAsset)] return Topology( topology_id=topology_id, grid_model_file=grid_model_file, diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index 64f470509..ca0028dd8 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -33,7 +33,13 @@ from toop_engine_interfaces.asset_topology.asset_topology import ( Topology, ) -from toop_engine_interfaces.asset_topology.assets import AssetBay, build_asset_bay_id, normalize_switchable_asset_payload +from toop_engine_interfaces.asset_topology.assets import ( + AssetBay, + BranchAsset, + InjectionAsset, + build_asset_bay_id, + normalize_switchable_asset_payload, +) from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.asset_topology.topology_conversion import topology_parts_from_materialized_station @@ -444,7 +450,9 @@ def _materialize_station_asset_connections( if "branch_end" in station_branches.columns else [None] * len(station_branches) ) - branch_mask = [asset.is_branch() is not False for asset in switchable_assets] + branch_mask = [isinstance(asset, BranchAsset) for asset in switchable_assets] + if any(not isinstance(asset, (BranchAsset, InjectionAsset)) for asset in switchable_assets): + raise ValueError("All station assets must normalize to BranchAsset or InjectionAsset") branch_connections: list[MaterializedAssetConnection] = [] injection_connections: list[MaterializedAssetConnection] = [] for asset, asset_terminal, asset_bay, is_branch in zip( @@ -603,9 +611,11 @@ def get_asset_topology_from_network( topology_assets = {} topology_asset_bays = {} for station in station_topologies: - topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_station, station_branch_assets, station_injection_assets, station_asset_bays = ( + topology_parts_from_materialized_station(station) + ) topology_stations.append(topology_station) - for asset in station_assets: + for asset in [*station_branch_assets, *station_injection_assets]: existing_asset = topology_assets.get(asset.grid_model_id) if existing_asset is None: topology_assets[asset.grid_model_id] = asset @@ -621,8 +631,8 @@ def get_asset_topology_from_network( raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") topology_assets_list = list(topology_assets.values()) - branch_assets = [asset for asset in topology_assets_list if asset.is_branch() is not False] - injection_assets = [asset for asset in topology_assets_list if asset.is_branch() is False] + branch_assets = [asset for asset in topology_assets_list if isinstance(asset, BranchAsset)] + injection_assets = [asset for asset in topology_assets_list if isinstance(asset, InjectionAsset)] return Topology( topology_id=topology_id, grid_model_file=grid_model_file, diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py index b204b1f93..51560eb02 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py @@ -11,7 +11,13 @@ from beartype.typing import Any, Optional, Union from pydantic import BaseModel, Field, field_validator, model_validator -from toop_engine_interfaces.asset_topology.assets import AssetBay, AssetSetpoint, SwitchableAsset +from toop_engine_interfaces.asset_topology.assets import ( + AssetBay, + AssetSetpoint, + BranchAsset, + InjectionAsset, + SwitchableAsset, +) from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.asset_topology.station_models import ( RawStation, @@ -64,10 +70,16 @@ class Topology(BaseModel): Each raw station represents one bus-branch bus view of a splitable station. """ - branch_assets: list[SwitchableAsset] = Field(default_factory=list) + circuit_groups: Optional[list[CircuitGroup]] = None + """The topology-owned circuit groups. The list contains groups of assets that are connected to each + other without power switches. This means in case of an outage, the fault current can flow through + all assets in the same circuit group, triggering their outage as well. + # TODO This is currently not implemented. Use a graph search to determine these.""" + + branch_assets: list[BranchAsset] = Field(default_factory=list) """The topology-owned canonical branch payloads.""" - injection_assets: list[SwitchableAsset] = Field(default_factory=list) + injection_assets: list[InjectionAsset] = Field(default_factory=list) """The topology-owned canonical injection payloads. Station-local branch-end and asset-bay assignment data are stored on raw_stations instead of on @@ -90,24 +102,20 @@ class Topology(BaseModel): @field_validator("branch_assets") @classmethod - def check_branch_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + def check_branch_asset_ids_unique(cls, v: list[BranchAsset]) -> list[BranchAsset]: """Check if all topology branch assets have unique grid model ids.""" asset_ids = [asset.grid_model_id for asset in v] if len(asset_ids) != len(set(asset_ids)): raise ValueError("grid_model_id must be unique for topology branch assets") - if any(asset.is_branch() is False for asset in v): - raise ValueError("branch_assets must not contain injection assets") return v @field_validator("injection_assets") @classmethod - def check_injection_asset_ids_unique(cls, v: list[SwitchableAsset]) -> list[SwitchableAsset]: + def check_injection_asset_ids_unique(cls, v: list[InjectionAsset]) -> list[InjectionAsset]: """Check if all topology injection assets have unique grid model ids.""" asset_ids = [asset.grid_model_id for asset in v] if len(asset_ids) != len(set(asset_ids)): raise ValueError("grid_model_id must be unique for topology injection assets") - if any(asset.is_branch() is True for asset in v): - raise ValueError("injection_assets must not contain branch assets") return v @field_validator("asset_bays") diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py index 9d1d55526..6a560fdf6 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/assets.py @@ -198,40 +198,18 @@ class SwitchableAsset(BaseModel): ignored. This shall not be used for elements intentionally disconnected, instead set all zeros in the switching table.""" - def is_branch(self) -> Optional[bool]: - """Return True if the asset is a branch. - - Only works if the type is set. If type is not set this will return None. - - Returns - ------- - bool - True if the asset is a branch, False if it is an injection. - """ - if self.asset_type is None: - return None - return self.asset_type in get_args(AssetBranchType) - class BranchAsset(SwitchableAsset): """Switchable asset representing a branch-type element.""" asset_type: Optional[AssetBranchType] = None - def is_branch(self) -> Optional[bool]: - """Return branch semantics for branch assets.""" - return None if self.asset_type is None else True - class InjectionAsset(SwitchableAsset): """Switchable asset representing an injection-type element.""" asset_type: Optional[AssetInjectionType] = None - def is_branch(self) -> Optional[bool]: - """Return branch semantics for injection assets.""" - return None if self.asset_type is None else False - def normalize_switchable_asset_payload(asset: dict[str, Any]) -> SwitchableAsset: """Normalize an asset payload to the matching branch or injection subclass when possible.""" diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py index c4cb5d29f..832edc0f6 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py @@ -9,14 +9,19 @@ from beartype.typing import Optional from toop_engine_interfaces.asset_topology.asset_topology import Topology -from toop_engine_interfaces.asset_topology.assets import AssetBay, SwitchableAsset, normalize_switchable_asset_payload +from toop_engine_interfaces.asset_topology.assets import ( + AssetBay, + BranchAsset, + InjectionAsset, + normalize_switchable_asset_payload, +) from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation from toop_engine_interfaces.asset_topology.station_models import RawStation, StationAssetConnection def topology_parts_from_materialized_station( station: MaterializedStation, -) -> tuple[RawStation, list[SwitchableAsset], list[AssetBay]]: +) -> tuple[RawStation, list[BranchAsset], list[InjectionAsset], list[AssetBay]]: """Extract topology-owned payloads from a materialized station. Parameters @@ -26,11 +31,11 @@ def topology_parts_from_materialized_station( Returns ------- - tuple[RawStation, list[SwitchableAsset], list[AssetBay]] - Raw station record plus topology-owned assets and asset bays derived from the station. + tuple[RawStation, list[BranchAsset], list[InjectionAsset], list[AssetBay]] + Raw station record plus topology-owned branch assets, injection assets, and asset bays. """ - branch_assets: list[SwitchableAsset] = [] - injection_assets: list[SwitchableAsset] = [] + branch_assets: list[BranchAsset] = [] + injection_assets: list[InjectionAsset] = [] asset_bays: list[AssetBay] = [] branch_connections: list[StationAssetConnection] = [] injection_connections: list[StationAssetConnection] = [] @@ -44,7 +49,8 @@ def topology_parts_from_materialized_station( asset_bay_id = asset_bay.asset_bay_id asset_bays.append(asset_bay.model_copy(deep=True)) - branch_assets.append(asset.model_copy(deep=True)) + branch_asset = asset if isinstance(asset, BranchAsset) else BranchAsset.model_validate(asset.model_dump()) + branch_assets.append(branch_asset.model_copy(deep=True)) branch_connections.append( StationAssetConnection( asset_id=asset_id, @@ -62,7 +68,8 @@ def topology_parts_from_materialized_station( if asset_bay is not None: asset_bays.append(asset_bay.model_copy(deep=True)) - injection_assets.append(asset.model_copy(deep=True)) + injection_asset = asset if isinstance(asset, InjectionAsset) else InjectionAsset.model_validate(asset.model_dump()) + injection_assets.append(injection_asset.model_copy(deep=True)) injection_connections.append( StationAssetConnection( asset_id=asset_id, @@ -88,7 +95,8 @@ def topology_parts_from_materialized_station( injection_connectivity=station.injection_connectivity, model_log=station.model_log, ), - [*branch_assets, *injection_assets], + branch_assets, + injection_assets, asset_bays, ) @@ -106,38 +114,24 @@ def topology_from_materialized_stations(reference_topology: Topology, stations: Returns ------- Topology - Topology containing raw stations and topology-owned payloads derived from ``stations``. + Topology containing updated raw stations while reusing the reference topology-owned payloads. """ topology_stations: list[RawStation] = [] - topology_branch_assets: dict[str, SwitchableAsset] = {} - topology_injection_assets: dict[str, SwitchableAsset] = {} - topology_asset_bays: dict[str, AssetBay] = {} for station in stations: - topology_station, station_assets, station_asset_bays = topology_parts_from_materialized_station(station) + topology_station, _station_branch_assets, _station_injection_assets, _station_asset_bays = ( + topology_parts_from_materialized_station(station) + ) topology_stations.append(topology_station) - for asset in station_assets: - topology_assets = topology_branch_assets if asset.is_branch() is not False else topology_injection_assets - existing_asset = topology_assets.get(asset.grid_model_id) - if existing_asset is None: - topology_assets[asset.grid_model_id] = asset - elif existing_asset != asset: - raise ValueError(f"Conflicting topology asset payload for grid_model_id {asset.grid_model_id}") - for asset_bay in station_asset_bays: - existing_asset_bay = topology_asset_bays.get(asset_bay.asset_bay_id) - if existing_asset_bay is None: - topology_asset_bays[asset_bay.asset_bay_id] = asset_bay - elif existing_asset_bay != asset_bay: - raise ValueError(f"Conflicting topology asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") return Topology( topology_id=reference_topology.topology_id, grid_model_file=reference_topology.grid_model_file, name=reference_topology.name, raw_stations=topology_stations, - branch_assets=list(topology_branch_assets.values()), - injection_assets=list(topology_injection_assets.values()), - asset_bays=list(topology_asset_bays.values()), + branch_assets=[asset.model_copy(deep=True) for asset in reference_topology.branch_assets], + injection_assets=[asset.model_copy(deep=True) for asset in reference_topology.injection_assets], + asset_bays=[asset_bay.model_copy(deep=True) for asset_bay in reference_topology.asset_bays], asset_setpoints=reference_topology.asset_setpoints, timestamp=reference_topology.timestamp, metrics=reference_topology.metrics, diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index 4c0f0a1ae..b40a1830c 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -525,6 +525,14 @@ def test_load_asset_topology() -> None: reference_topology=Topology( topology_id="topology1", raw_stations=[], + branch_assets=[ + BranchAsset(grid_model_id="line1"), + BranchAsset(grid_model_id="line2"), + BranchAsset(grid_model_id="line3"), + BranchAsset(grid_model_id="line4"), + BranchAsset(grid_model_id="line5"), + BranchAsset(grid_model_id="line6"), + ], timestamp=datetime.now(), ), stations=[station1, station2], @@ -573,6 +581,9 @@ def test_topology_extracts_assets_and_materializes_stations() -> None: reference_topology=Topology( topology_id="topology1", raw_stations=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + injection_assets=[InjectionAsset(grid_model_id="load1", asset_type="load")], + asset_bays=[asset_bays[0]], timestamp=datetime.now(), ), stations=[station], @@ -622,6 +633,7 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw reference_topology=Topology( topology_id="topology1", raw_stations=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], timestamp=datetime.now(), ), stations=[station_from, station_to], @@ -657,6 +669,7 @@ def test_topology_from_materialized_stations_normalizes_equivalent_branch_asset_ reference_topology=Topology( topology_id="topology1", raw_stations=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], timestamp=datetime.now(), ), stations=[station_from, station_to], @@ -667,6 +680,67 @@ def test_topology_from_materialized_stations_normalizes_equivalent_branch_asset_ assert topology.branch_assets[0].grid_model_id == "line1" +def test_topology_from_materialized_stations_reuses_reference_canonical_assets() -> None: + reference_topology = Topology( + topology_id="topology1", + raw_stations=[], + branch_assets=[ + BranchAsset(grid_model_id="line1", asset_type="line"), + BranchAsset(grid_model_id="line_unused", asset_type="line"), + ], + injection_assets=[InjectionAsset(grid_model_id="load1", asset_type="load")], + asset_bays=[ + AssetBay( + asset_bay_id="station1::line1::bay", + dv_switch_grid_model_id="dv1", + sr_switch_grid_model_id={"busbar1": "sr1"}, + ) + ], + timestamp=datetime.now(), + ) + station = make_materialized_station( + busbars=[Busbar(int_id=1, grid_model_id="busbar1")], + couplers=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + branch_terminals=["from"], + branch_asset_bays=[reference_topology.asset_bays[0]], + branch_switching_table=np.array([[True]]), + injection_assets=[InjectionAsset(grid_model_id="load1", asset_type="load")], + injection_switching_table=np.array([[True]]), + grid_model_id="station1", + ) + + topology = topology_from_materialized_stations(reference_topology=reference_topology, stations=[station]) + + assert [asset.grid_model_id for asset in topology.branch_assets] == ["line1", "line_unused"] + assert [asset.grid_model_id for asset in topology.injection_assets] == ["load1"] + assert [asset_bay.asset_bay_id for asset_bay in topology.asset_bays] == ["station1::line1::bay"] + assert topology.raw_stations[0].branch_connections[0].asset_id == "line1" + + +def test_topology_from_materialized_stations_raises_when_reference_assets_are_missing() -> None: + reference_topology = Topology( + topology_id="topology1", + raw_stations=[], + branch_assets=[], + injection_assets=[], + asset_bays=[], + timestamp=datetime.now(), + ) + station = make_materialized_station( + busbars=[Busbar(int_id=1, grid_model_id="busbar1")], + couplers=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + branch_switching_table=np.array([[True]]), + grid_model_id="station1", + ) + + with pytest.raises( + ValidationError, match="Branch asset grid_model_id line1 referenced by station station1 does not exist" + ): + topology_from_materialized_stations(reference_topology=reference_topology, stations=[station]) + + def test_raw_station_model_copy_revalidates_updates() -> None: station = make_raw_station( grid_model_id="station1", @@ -732,6 +806,11 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ reference_topology=Topology( topology_id="topology1", raw_stations=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + asset_bays=[ + station_from.branch_connections[0].asset_bay.model_copy(deep=True), + station_to.branch_connections[0].asset_bay.model_copy(deep=True), + ], timestamp=datetime.now(), ), stations=[station_from, station_to], @@ -777,6 +856,11 @@ def test_topology_from_materialized_stations_scopes_generated_asset_bay_ids_per_ reference_topology=Topology( topology_id="topology1", raw_stations=[], + branch_assets=[BranchAsset(grid_model_id="line1", asset_type="line")], + asset_bays=[ + station.branch_connections[0].asset_bay.model_copy(deep=True), + station.branch_connections[1].asset_bay.model_copy(deep=True), + ], timestamp=datetime.now(), ), stations=[station], @@ -1206,13 +1290,10 @@ def test_station_bay() -> None: def test_disambiguate_type() -> None: asset = normalize_switchable_asset_payload({"grid_model_id": "line", "asset_type": None}) - assert asset.is_branch() is None assert type(asset) is SwitchableAsset asset = normalize_switchable_asset_payload({"grid_model_id": "line", "asset_type": "line"}) - assert asset.is_branch() is True assert isinstance(asset, BranchAsset) asset = normalize_switchable_asset_payload({"grid_model_id": "gen", "asset_type": "gen"}) - assert asset.is_branch() is False assert isinstance(asset, InjectionAsset) From b09dee66ff5b4e3823d72b9613662a6717ce8e78 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Tue, 23 Jun 2026 12:22:36 +0200 Subject: [PATCH 21/24] fix: rename terminal to branch_end Signed-off-by: Leonard Hilfrich --- .../toop_engine_dc_solver/example_classes.py | 6 +-- .../toop_engine_dc_solver/example_grids.py | 47 +++++++++++++++---- .../postprocess/postprocess_pandapower.py | 2 +- .../postprocess/postprocess_powsybl.py | 2 +- .../preprocess/preprocess_switching.py | 2 +- .../test_preprocess_switching.py | 8 ++-- .../powsybl/powsybl_asset_topo.py | 46 +++++++++--------- .../pandapower_import/asset_topology.py | 2 +- .../test_powsybl_station_to_graph.py | 2 +- .../pypowsybl_import/test_preprocessing.py | 4 +- .../asset_topology/asset_topology.py | 4 +- .../asset_topology/materialized_topology.py | 4 +- .../asset_topology/station_models.py | 6 +-- .../asset_topology/topology_conversion.py | 4 +- .../tests/test_asset_topology.py | 8 ++-- .../tests/test_asset_topology_helpers.py | 8 ++-- .../tests/test_stored_action_set.py | 28 ++++++----- .../test_stored_action_set_performance.py | 8 ++-- 18 files changed, 113 insertions(+), 78 deletions(-) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py index 3259e268c..c74198a9d 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/example_classes.py @@ -54,9 +54,9 @@ def get_basic_node_breaker_topology() -> Topology: ) ], branch_connections=[ - StationAssetConnection(asset_id="L4", terminal=None, asset_bay_id="VL4_0::L4::bay"), - StationAssetConnection(asset_id="L5", terminal=None, asset_bay_id="VL4_0::L5::bay"), - StationAssetConnection(asset_id="L8", terminal=None, asset_bay_id="VL4_0::L8::bay"), + StationAssetConnection(asset_id="L4", branch_end=None, asset_bay_id="VL4_0::L4::bay"), + StationAssetConnection(asset_id="L5", branch_end=None, asset_bay_id="VL4_0::L5::bay"), + StationAssetConnection(asset_id="L8", branch_end=None, asset_bay_id="VL4_0::L8::bay"), ], injection_connections=[], branch_switching_table=np.array([[False, False, False], [True, True, False]], dtype=bool), 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 abbea5db0..3f145824e 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 @@ -56,7 +56,9 @@ ) from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, InjectionAsset from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation -from toop_engine_interfaces.asset_topology.topology_conversion import topology_from_materialized_stations +from toop_engine_interfaces.asset_topology.topology_conversion import ( + topology_parts_from_materialized_station, +) from toop_engine_interfaces.backend import BackendInterface from toop_engine_interfaces.folder_structure import ( NETWORK_MASK_NAMES, @@ -292,6 +294,7 @@ def random_station_info_backend( ), pp_counters +# ruff: noqa: C901 def random_topology_info_backend(backend: BackendInterface, pp_counters: Optional[PandapowerCounters]) -> Topology: """Generate a random topology for any backend @@ -315,13 +318,41 @@ def random_topology_info_backend(backend: BackendInterface, pp_counters: Optiona new_station, pp_counters = random_station_info_backend(backend, node_idx, pp_counters) stations.append(new_station) - return topology_from_materialized_stations( - reference_topology=Topology( - topology_id="random_topology", - raw_stations=[], - timestamp=datetime.datetime.now(), - ), - stations=stations, + raw_stations = [] + branch_assets: dict[str, BranchAsset] = {} + injection_assets: dict[str, InjectionAsset] = {} + asset_bays = {} + for station in stations: + raw_station, station_branch_assets, station_injection_assets, station_asset_bays = ( + topology_parts_from_materialized_station(station) + ) + raw_stations.append(raw_station) + for asset in station_branch_assets: + existing_asset = branch_assets.get(asset.grid_model_id) + if existing_asset is None: + branch_assets[asset.grid_model_id] = asset + elif existing_asset != asset: + raise ValueError(f"Conflicting branch asset payload for grid_model_id {asset.grid_model_id}") + for asset in station_injection_assets: + existing_asset = injection_assets.get(asset.grid_model_id) + if existing_asset is None: + injection_assets[asset.grid_model_id] = asset + elif existing_asset != asset: + raise ValueError(f"Conflicting injection asset payload for grid_model_id {asset.grid_model_id}") + for asset_bay in station_asset_bays: + existing_asset_bay = asset_bays.get(asset_bay.asset_bay_id) + if existing_asset_bay is None: + asset_bays[asset_bay.asset_bay_id] = asset_bay + elif existing_asset_bay != asset_bay: + raise ValueError(f"Conflicting asset bay payload for asset_bay_id {asset_bay.asset_bay_id}") + + return Topology( + topology_id="random_topology", + raw_stations=raw_stations, + branch_assets=list(branch_assets.values()), + injection_assets=list(injection_assets.values()), + asset_bays=list(asset_bays.values()), + timestamp=datetime.datetime.now(), ) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py index cd7222b0a..190783633 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_pandapower.py @@ -209,7 +209,7 @@ def compute_cross_coupler_flows( if station.branch_switching_table[busbars_a, index].any(): asset_id, asset_type = parse_globally_unique_id(asset_connection.asset.grid_model_id) asset_id = int(asset_id) - branch_end = asset_connection.terminal + branch_end = asset_connection.branch_end if branch_end is None: raise ValueError("Branch end must be set in asset topo") from_end = branch_end in ("from", "hv") diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index ac05bfef5..a57383446 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -268,7 +268,7 @@ def compute_cross_coupler_flows( q_sum = 0.0 for index, asset_connection in enumerate(station.branch_connections): if station.branch_switching_table[busbars_a, index].any(): - branch_end = asset_connection.terminal + branch_end = asset_connection.branch_end if branch_end is None: raise ValueError("Branch end is None") from_end = branch_end in ("from", "hv") diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py index 893e91a89..0cfc99c77 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/preprocess/preprocess_switching.py @@ -444,7 +444,7 @@ def add_missing_asset_topology_branch_info( new_branch_ends = [] for asset_connection in station.branch_connections: asset_id = asset_connection.asset_id - branch_end = asset_connection.terminal + branch_end = asset_connection.branch_end index = branch_id_lookup.get(asset_id, None) if ( index is not None diff --git a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py index 05108a485..7f079c7a1 100644 --- a/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py +++ b/packages/dc_solver_pkg/tests/preprocessing/test_preprocess_switching.py @@ -225,8 +225,8 @@ def test_add_missing_asset_topology_branch_info(network_data: NetworkData) -> No if asset.grid_model_id in network_data.branch_ids: assert asset.name in network_data.branch_names assert asset.asset_type in network_data.branch_types - assert asset_connection.terminal in ["from", "to"] - if asset_connection.terminal == "from": + assert asset_connection.branch_end in ["from", "to"] + if asset_connection.branch_end == "from": from_ends += 1 else: to_ends += 1 @@ -311,12 +311,12 @@ def test_prepare_for_separation_set_node_breaker_test_station(): asset_bays = station_data.pop("asset_bays", [None] * len(assets)) combined_switching_table = np.asarray(station_data.pop("asset_switching_table"), dtype=bool) station_data["branch_connections"] = [ - {"asset": asset, "terminal": asset_terminal, "asset_bay": asset_bay} + {"asset": asset, "branch_end": asset_terminal, "asset_bay": asset_bay} for asset, asset_terminal, asset_bay in zip(assets, asset_terminals, asset_bays, strict=True) if asset["asset_type"] in get_args(AssetBranchType) ] station_data["injection_connections"] = [ - {"asset": asset, "terminal": asset_terminal, "asset_bay": asset_bay} + {"asset": asset, "branch_end": asset_terminal, "asset_bay": asset_bay} for asset, asset_terminal, asset_bay in zip(assets, asset_terminals, asset_bays, strict=True) if asset["asset_type"] in get_args(AssetInjectionType) ] diff --git a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py index b16fe66d2..bbf6802ee 100644 --- a/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py +++ b/packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py @@ -194,8 +194,8 @@ def _get_station_asset_inputs_from_topology( ------- station_elements : pd.DataFrame Normalized station asset rows used for asset materialization. - asset_terminals : list[str | None] - Terminal labels aligned with `station_elements`. + asset_branch_ends : list[str | None] + Branch-end labels aligned with `station_elements`. switching_matrix : np.ndarray Station-local switching matrix aligned with `station_elements`. asset_connectivity : np.ndarray @@ -208,17 +208,17 @@ def _get_station_asset_inputs_from_topology( element_names, ) asset_connectivity = np.ones_like(switching_matrix, dtype=bool) - asset_terminals = ( + asset_branch_ends = ( station_elements["branch_end"].tolist() if "branch_end" in station_elements.columns else [None] * len(station_elements) ) - return station_elements, asset_terminals, switching_matrix, asset_connectivity + return station_elements, asset_branch_ends, switching_matrix, asset_connectivity def _get_branch_station_assets_from_df( station_elements: pd.DataFrame, - asset_terminals: list[str | None], + asset_branch_ends: list[str | None], switching_matrix: np.ndarray, asset_connectivity: np.ndarray, ) -> tuple[list[BranchAsset], list[str | None], np.ndarray, np.ndarray]: @@ -228,8 +228,8 @@ def _get_branch_station_assets_from_df( ---------- station_elements : pd.DataFrame Normalized station asset rows. - asset_terminals : list[str | None] - Terminal labels aligned with `station_elements`. + asset_branch_ends : list[str | None] + Branch-end labels aligned with `station_elements`. switching_matrix : np.ndarray Station-local switching matrix aligned with `station_elements`. asset_connectivity : np.ndarray @@ -239,8 +239,8 @@ def _get_branch_station_assets_from_df( ------- branch_assets : list[BranchAsset] Materialized branch assets for the station. - branch_terminals : list[str | None] - Terminal labels aligned with `branch_assets`. + branch_ends : list[str | None] + Branch-end labels aligned with `branch_assets`. branch_switching_table : np.ndarray Branch-only switching table. branch_connectivity : np.ndarray @@ -258,16 +258,16 @@ def _get_branch_station_assets_from_df( for asset, is_branch in zip(normalized_assets, branch_mask, strict=True) if is_branch ] - branch_terminals = [terminal for terminal, is_branch in zip(asset_terminals, branch_mask, strict=True) if is_branch] + branch_ends = [branch_end for branch_end, is_branch in zip(asset_branch_ends, branch_mask, strict=True) if is_branch] branch_switching_table = switching_matrix[:, branch_mask] branch_connectivity = asset_connectivity[:, branch_mask] - return branch_assets, branch_terminals, branch_switching_table, branch_connectivity + return branch_assets, branch_ends, branch_switching_table, branch_connectivity def _get_injection_station_assets_from_df( station_elements: pd.DataFrame, - asset_terminals: list[str | None], + asset_branch_ends: list[str | None], switching_matrix: np.ndarray, asset_connectivity: np.ndarray, ) -> tuple[list[InjectionAsset], list[str | None], np.ndarray, np.ndarray]: @@ -277,8 +277,8 @@ def _get_injection_station_assets_from_df( ---------- station_elements : pd.DataFrame Normalized station asset rows. - asset_terminals : list[str | None] - Terminal labels aligned with `station_elements`. + asset_branch_ends : list[str | None] + Branch-end labels aligned with `station_elements`. switching_matrix : np.ndarray Station-local switching matrix aligned with `station_elements`. asset_connectivity : np.ndarray @@ -288,8 +288,8 @@ def _get_injection_station_assets_from_df( ------- injection_assets : list[InjectionAsset] Materialized injection assets for the station. - injection_terminals : list[str | None] - Terminal labels aligned with `injection_assets`. + injection_branch_ends : list[str | None] + Branch-end labels aligned with `injection_assets`. injection_switching_table : np.ndarray Injection-only switching table. injection_connectivity : np.ndarray @@ -307,13 +307,13 @@ def _get_injection_station_assets_from_df( for asset, is_injection in zip(normalized_assets, injection_mask, strict=True) if is_injection ] - injection_terminals = [ - terminal for terminal, is_injection in zip(asset_terminals, injection_mask, strict=True) if is_injection + injection_branch_ends = [ + branch_end for branch_end, is_injection in zip(asset_branch_ends, injection_mask, strict=True) if is_injection ] injection_switching_table = switching_matrix[:, injection_mask] injection_connectivity = asset_connectivity[:, injection_mask] - return injection_assets, injection_terminals, injection_switching_table, injection_connectivity + return injection_assets, injection_branch_ends, injection_switching_table, injection_connectivity def _get_single_topology_kind(buses_with_substation_and_voltage: pd.DataFrame) -> str: @@ -660,11 +660,11 @@ def get_raw_stations_and_assets( busbars=get_list_of_busbars_from_df(station_buses), couplers=get_list_of_coupler_from_df(coupler_elements), branch_connections=[ - StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) + StationAssetConnection(asset_id=asset.grid_model_id, branch_end=asset_terminal, asset_bay_id=None) for asset, asset_terminal in zip(branch_assets, branch_terminals, strict=True) ], injection_connections=[ - StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) + StationAssetConnection(asset_id=asset.grid_model_id, branch_end=asset_terminal, asset_bay_id=None) for asset, asset_terminal in zip(injection_assets, injection_terminals, strict=True) ], branch_switching_table=branch_switching_table, @@ -823,11 +823,11 @@ def get_raw_stations_and_assets_bus_breaker( busbars=busbars, couplers=couplers, branch_connections=[ - StationAssetConnection(asset_id=asset.grid_model_id, terminal=asset_terminal, asset_bay_id=None) + StationAssetConnection(asset_id=asset.grid_model_id, branch_end=asset_terminal, asset_bay_id=None) for asset, asset_terminal in zip(branch_assets, branch_terminals, strict=True) ], injection_connections=[ - StationAssetConnection(asset_id=asset.grid_model_id, terminal=None, asset_bay_id=None) + StationAssetConnection(asset_id=asset.grid_model_id, branch_end=None, asset_bay_id=None) for asset in injection_assets ], branch_switching_table=branch_switching_table, diff --git a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py index ca0028dd8..083268220 100644 --- a/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py +++ b/packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py @@ -462,7 +462,7 @@ def _materialize_station_asset_connections( branch_mask, strict=True, ): - connection = MaterializedAssetConnection(asset=asset, terminal=asset_terminal, asset_bay=asset_bay) + connection = MaterializedAssetConnection(asset=asset, branch_end=asset_terminal, asset_bay=asset_bay) if is_branch: branch_connections.append(connection) else: 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 2d1d1bafc..dde360757 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 @@ -175,7 +175,7 @@ def test_get_station(basic_node_breaker_network_powsybl_grid: Network): assets = [asset_connection.asset for asset_connection in all_station_connections(res)] asset_bays = [asset_connection.asset_bay for asset_connection in all_station_connections(res)] - asset_terminals = [asset_connection.terminal for asset_connection in all_station_connections(res)] + asset_terminals = [asset_connection.branch_end for asset_connection in all_station_connections(res)] assert len(assets) == 5 assert asset_terminals == [None] * len(assets) assert assets[0].grid_model_id == "L3" diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index 5e43caf0c..2758e31e6 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -71,7 +71,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow Busbar(int_id=1, grid_model_id="bb1", bus_branch_bus_id=buses.index[1]), ], couplers=[], - branch_connections=[StationAssetConnection(asset_id="asset0", terminal=None, asset_bay_id=None)], + branch_connections=[StationAssetConnection(asset_id="asset0", branch_end=None, asset_bay_id=None)], injection_connections=[], branch_switching_table=np.array([[True], [False]]), injection_switching_table=np.zeros((2, 0), dtype=bool), @@ -81,7 +81,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow name="regular", busbars=[Busbar(int_id=0, grid_model_id="bb2", bus_branch_bus_id=buses.index[1])], couplers=[], - branch_connections=[StationAssetConnection(asset_id="asset1", terminal=None, asset_bay_id=None)], + branch_connections=[StationAssetConnection(asset_id="asset1", branch_end=None, asset_bay_id=None)], injection_connections=[], branch_switching_table=np.array([[True]]), injection_switching_table=np.zeros((1, 0), dtype=bool), diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py index 51560eb02..f0cd8ca05 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py @@ -203,7 +203,7 @@ def materialize_stations(self) -> list[MaterializedStation]: branch_connections=[ MaterializedAssetConnection( asset=asset, - terminal=asset_connection.terminal, + branch_end=asset_connection.branch_end, asset_bay=asset_bay, ) for asset, asset_connection, asset_bay in zip( @@ -216,7 +216,7 @@ def materialize_stations(self) -> list[MaterializedStation]: injection_connections=[ MaterializedAssetConnection( asset=asset, - terminal=asset_connection.terminal, + branch_end=asset_connection.branch_end, asset_bay=asset_bay, ) for asset, asset_connection, asset_bay in zip( diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py index 824b429e1..7e341f087 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py @@ -30,8 +30,8 @@ class MaterializedAssetConnection(BaseModel): asset: SwitchableAsset """Station-local asset payload aligned with one switching-table column.""" - terminal: Optional[BranchEnd] = None - """Optional branch terminal metadata for this station-local asset occurrence.""" + branch_end: Optional[BranchEnd] = None + """Optional branch-end metadata for this station-local asset occurrence.""" asset_bay: Optional[AssetBay] = None """Optional station-local asset bay payload for this station-local asset occurrence.""" diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py index 993cdfeed..50472cf36 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/station_models.py @@ -35,8 +35,8 @@ class StationAssetConnection(BaseModel): asset_id: str """Grid model id of the topology-owned asset referenced by this station-local column.""" - terminal: Optional[BranchEnd] = None - """Optional branch terminal metadata for this station-local asset occurrence.""" + branch_end: Optional[BranchEnd] = None + """Optional branch-end metadata for this station-local asset occurrence.""" asset_bay_id: Optional[str] = None """Optional topology-scoped asset bay identifier for this station-local asset occurrence.""" @@ -304,7 +304,7 @@ def with_asset_terminals(self, asset_terminals: list[Optional[BranchEnd]]) -> "R return self.model_copy( update={ "branch_connections": [ - asset_connection.model_copy(update={"terminal": asset_terminal}) + asset_connection.model_copy(update={"branch_end": asset_terminal}) for asset_connection, asset_terminal in zip(self.branch_connections, asset_terminals, strict=True) ] } diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py index 832edc0f6..1d787e289 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/topology_conversion.py @@ -54,7 +54,7 @@ def topology_parts_from_materialized_station( branch_connections.append( StationAssetConnection( asset_id=asset_id, - terminal=asset_connection.terminal, + branch_end=asset_connection.branch_end, asset_bay_id=asset_bay_id, ) ) @@ -73,7 +73,7 @@ def topology_parts_from_materialized_station( injection_connections.append( StationAssetConnection( asset_id=asset_id, - terminal=asset_connection.terminal, + branch_end=asset_connection.branch_end, asset_bay_id=asset_bay_id, ) ) diff --git a/packages/interfaces_pkg/tests/test_asset_topology.py b/packages/interfaces_pkg/tests/test_asset_topology.py index b40a1830c..8174f7516 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology.py +++ b/packages/interfaces_pkg/tests/test_asset_topology.py @@ -51,7 +51,7 @@ def materialized_asset_connections( if asset_bays is None: asset_bays = [None] * len(assets) return [ - MaterializedAssetConnection(asset=asset, terminal=terminal, asset_bay=asset_bay) + MaterializedAssetConnection(asset=asset, branch_end=terminal, asset_bay=asset_bay) for asset, terminal, asset_bay in zip(assets, terminals, asset_bays, strict=True) ] @@ -66,7 +66,7 @@ def raw_asset_connections( if asset_bay_ids is None: asset_bay_ids = [None] * len(asset_ids) return [ - StationAssetConnection(asset_id=asset_id, terminal=terminal, asset_bay_id=asset_bay_id) + StationAssetConnection(asset_id=asset_id, branch_end=terminal, asset_bay_id=asset_bay_id) for asset_id, terminal, asset_bay_id in zip(asset_ids, terminals, asset_bay_ids, strict=True) ] @@ -641,9 +641,9 @@ def test_topology_from_materialized_stations_keeps_single_canonical_asset_for_tw assert [asset.grid_model_id for asset in topology.branch_assets] == ["line1"] assert [connection.asset_id for connection in topology.raw_stations[0].branch_connections] == ["line1"] - assert [connection.terminal for connection in topology.raw_stations[0].branch_connections] == ["from"] + assert [connection.branch_end for connection in topology.raw_stations[0].branch_connections] == ["from"] assert [connection.asset_id for connection in topology.raw_stations[1].branch_connections] == ["line1"] - assert [connection.terminal for connection in topology.raw_stations[1].branch_connections] == ["to"] + assert [connection.branch_end for connection in topology.raw_stations[1].branch_connections] == ["to"] assert topology.materialize_stations() == [station_from, station_to] diff --git a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py index fa813d19a..567f0dd8c 100644 --- a/packages/interfaces_pkg/tests/test_asset_topology_helpers.py +++ b/packages/interfaces_pkg/tests/test_asset_topology_helpers.py @@ -84,7 +84,7 @@ def build_raw_station( busbars=busbars, couplers=couplers, branch_connections=[ - StationAssetConnection(asset_id=asset_id, terminal=asset_terminal, asset_bay_id=asset_bay_id) + StationAssetConnection(asset_id=asset_id, branch_end=asset_terminal, asset_bay_id=asset_bay_id) for asset_id, asset_terminal, asset_bay_id in zip( asset_ids, asset_terminals if asset_terminals is not None else [None] * len(asset_ids), @@ -93,7 +93,7 @@ def build_raw_station( ) ], injection_connections=[ - StationAssetConnection(asset_id=asset_id, terminal=None, asset_bay_id=None) + StationAssetConnection(asset_id=asset_id, branch_end=None, asset_bay_id=None) for asset_id in resolved_injection_asset_ids ], branch_switching_table=asset_switching_table, @@ -451,8 +451,8 @@ def test_get_connected_assets_raw_station_injection_scope(): Busbar(int_id=2, grid_model_id="busbar2"), ], couplers=[], - branch_connections=[StationAssetConnection(asset_id="line1", terminal="from", asset_bay_id=None)], - injection_connections=[StationAssetConnection(asset_id="gen1", terminal=None, asset_bay_id=None)], + branch_connections=[StationAssetConnection(asset_id="line1", branch_end="from", asset_bay_id=None)], + injection_connections=[StationAssetConnection(asset_id="gen1", branch_end=None, asset_bay_id=None)], branch_switching_table=np.array([[True], [False]]), injection_switching_table=np.array([[False], [True]]), ) diff --git a/packages/interfaces_pkg/tests/test_stored_action_set.py b/packages/interfaces_pkg/tests/test_stored_action_set.py index 71183dd18..1d5608fc1 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set.py @@ -84,7 +84,7 @@ def build_raw_station( busbars=busbars, couplers=couplers, branch_connections=[ - StationAssetConnection(asset_id=asset_id, terminal=asset_terminal, asset_bay_id=asset_bay_id) + StationAssetConnection(asset_id=asset_id, branch_end=asset_terminal, asset_bay_id=asset_bay_id) for asset_id, asset_terminal, asset_bay_id in zip( asset_ids, asset_terminals if asset_terminals is not None else [None] * len(asset_ids), @@ -93,7 +93,7 @@ def build_raw_station( ) ], injection_connections=[ - StationAssetConnection(asset_id=asset_id, terminal=None, asset_bay_id=None) + StationAssetConnection(asset_id=asset_id, branch_end=None, asset_bay_id=None) for asset_id in resolved_injection_asset_ids ], branch_switching_table=asset_switching_table, @@ -129,10 +129,10 @@ def build_materialized_station( busbars=busbars, couplers=couplers, branch_connections=[ - MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + MaterializedAssetConnection.model_construct(asset=asset, branch_end=None, asset_bay=None) for asset in assets ], injection_connections=[ - MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) + MaterializedAssetConnection.model_construct(asset=asset, branch_end=None, asset_bay=None) for asset in resolved_injection_assets ], branch_switching_table=asset_switching_table, @@ -396,7 +396,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): ) ] assets = [ - SwitchableAsset.model_construct( + BranchAsset.model_construct( grid_model_id="station_a_asset_0", type=None, name=None, @@ -412,7 +412,7 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - branch_connections=[MaterializedAssetConnection.model_construct(asset=assets[0], terminal=None, asset_bay=None)], + branch_connections=[MaterializedAssetConnection.model_construct(asset=assets[0], branch_end=None, asset_bay=None)], injection_connections=[], branch_switching_table=np.zeros((1, 1), dtype=bool), injection_switching_table=np.zeros((1, 0), dtype=bool), @@ -448,7 +448,9 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - branch_connections=[StationAssetConnection(asset_id="station_a_asset_0", terminal=None, asset_bay_id=None)], + branch_connections=[ + StationAssetConnection(asset_id="station_a_asset_0", branch_end=None, asset_bay_id=None) + ], injection_connections=[], branch_switching_table=np.zeros((1, 1), dtype=bool), injection_switching_table=np.zeros((1, 0), dtype=bool), @@ -464,7 +466,9 @@ def test_action_set_model_validator_rejects_non_grouped_local_actions(): voltage_level=None, busbars=busbars, couplers=[], - branch_connections=[StationAssetConnection(asset_id="station_b_asset_0", terminal=None, asset_bay_id=None)], + branch_connections=[ + StationAssetConnection(asset_id="station_b_asset_0", branch_end=None, asset_bay_id=None) + ], injection_connections=[], branch_switching_table=np.zeros((1, 1), dtype=bool), injection_switching_table=np.zeros((1, 0), dtype=bool), @@ -670,13 +674,13 @@ def test_compress_station_diffs_raises_on_non_diff_hypothesis_change(): ) ] assets = [ - SwitchableAsset.model_construct( + BranchAsset.model_construct( grid_model_id="station_save_load_asset_1", type=None, name=None, in_service=True, ), - SwitchableAsset.model_construct( + BranchAsset.model_construct( grid_model_id="station_save_load_asset_2", type=None, name=None, @@ -784,13 +788,13 @@ def test_save_and_load_action_set_split_files_roundtrip(tmp_path: Path): ) ] assets = [ - SwitchableAsset.model_construct( + BranchAsset.model_construct( grid_model_id="asset1", type=None, name=None, in_service=True, ), - SwitchableAsset.model_construct( + BranchAsset.model_construct( grid_model_id="asset2", type=None, name=None, diff --git a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py index 09352bf7a..37b247dc4 100644 --- a/packages/interfaces_pkg/tests/test_stored_action_set_performance.py +++ b/packages/interfaces_pkg/tests/test_stored_action_set_performance.py @@ -16,7 +16,7 @@ RawStation, Topology, ) -from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler, SwitchableAsset +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar, BusbarCoupler from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection from toop_engine_interfaces.filesystem_helper import save_pydantic_model_fs @@ -53,7 +53,7 @@ def _build_large_random_action_set( asset_counts = np.tile(asset_counts, n_stations // len(asset_counts)) raw_stations: list[RawStation] = [] - topology_assets: list[SwitchableAsset] = [] + topology_assets: list[BranchAsset] = [] local_actions: list[MaterializedStation] = [] for station_idx in range(n_stations): @@ -111,7 +111,7 @@ def _build_large_random_action_set( branch_connections=[ StationAssetConnection.model_construct( asset_id=asset.grid_model_id, - terminal=None, + branch_end=None, asset_bay_id=None, ) for asset in assets @@ -135,7 +135,7 @@ def _build_large_random_action_set( busbars=busbars, couplers=couplers, branch_connections=[ - MaterializedAssetConnection.model_construct(asset=asset, terminal=None, asset_bay=None) for asset in assets + MaterializedAssetConnection.model_construct(asset=asset, branch_end=None, asset_bay=None) for asset in assets ], branch_switching_table=branch_switching_table, branch_connectivity=None, From 82585cb388d8fedc73409ccfa2083973f202084b Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Tue, 23 Jun 2026 12:32:15 +0200 Subject: [PATCH 22/24] fix: failing notebook Signed-off-by: Leonard Hilfrich --- .../export/disconnection_switch_updates.py | 8 +++++- .../asset_topology/asset_topology.py | 28 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py index 3870f4d9b..50339e74d 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/disconnection_switch_updates.py @@ -14,6 +14,7 @@ from toop_engine_interfaces.asset_topology.asset_topology import Topology from toop_engine_interfaces.asset_topology.assets import AssetBay from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation +from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.nminus1_definition import GridElement from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema @@ -105,4 +106,9 @@ def get_changing_switches_from_disconnections( } ) - return pd.DataFrame.from_records(switch_updates, columns=["grid_model_id", "open"]) + if not switch_updates: + return get_empty_dataframe_from_model(SwitchUpdateSchema) + + return pd.DataFrame.from_records(switch_updates, columns=["grid_model_id", "open"]).astype( + {"grid_model_id": str, "open": bool} + ) diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py index f0cd8ca05..b8e7ce382 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py @@ -169,6 +169,8 @@ def materialize_stations(self) -> list[MaterializedStation]: materialized_stations: list[MaterializedStation] = [] for station in self.raw_stations: + station_busbar_ids = {busbar.grid_model_id for busbar in station.busbars} + station_branch_assets = [ branch_asset_map[asset_connection.asset_id].model_copy(deep=True) for asset_connection in station.branch_connections @@ -179,13 +181,35 @@ def materialize_stations(self) -> list[MaterializedStation]: ] station_branch_asset_bays = [ - asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) + asset_bay_map[asset_connection.asset_bay_id].model_copy( + update={ + "sr_switch_grid_model_id": { + busbar_id: switch_id + for busbar_id, switch_id in asset_bay_map[ + asset_connection.asset_bay_id + ].sr_switch_grid_model_id.items() + if busbar_id in station_busbar_ids + } + }, + deep=True, + ) if asset_connection.asset_bay_id is not None else None for asset_connection in station.branch_connections ] station_injection_asset_bays = [ - asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) + asset_bay_map[asset_connection.asset_bay_id].model_copy( + update={ + "sr_switch_grid_model_id": { + busbar_id: switch_id + for busbar_id, switch_id in asset_bay_map[ + asset_connection.asset_bay_id + ].sr_switch_grid_model_id.items() + if busbar_id in station_busbar_ids + } + }, + deep=True, + ) if asset_connection.asset_bay_id is not None else None for asset_connection in station.injection_connections From f5090f84f0b15d6201a974da3046615001478117 Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Tue, 23 Jun 2026 12:37:35 +0200 Subject: [PATCH 23/24] fix: importer tests Signed-off-by: Leonard Hilfrich --- .../importer_pkg/tests/pypowsybl_import/test_preprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py index 2758e31e6..bb34c32e5 100644 --- a/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py +++ b/packages/importer_pkg/tests/pypowsybl_import/test_preprocessing.py @@ -29,7 +29,7 @@ RawStation, Topology, ) -from toop_engine_interfaces.asset_topology.assets import Busbar, SwitchableAsset +from toop_engine_interfaces.asset_topology.assets import BranchAsset, Busbar from toop_engine_interfaces.asset_topology.station_models import StationAssetConnection from toop_engine_interfaces.folder_structure import ( NETWORK_MASK_NAMES, @@ -90,7 +90,7 @@ def test_filter_split_stations_from_relevant_subs(basic_node_breaker_network_pow topology_id="topology", grid_model_file="grid.xiidm", raw_stations=[split_station, regular_station], - branch_assets=[SwitchableAsset(grid_model_id="asset0"), SwitchableAsset(grid_model_id="asset1")], + branch_assets=[BranchAsset(grid_model_id="asset0"), BranchAsset(grid_model_id="asset1")], injection_assets=[], timestamp=datetime.now(), ) From b3feebfb3403054d69564236f2656f18c1a856fa Mon Sep 17 00:00:00 2001 From: Leonard Hilfrich Date: Tue, 23 Jun 2026 12:57:26 +0200 Subject: [PATCH 24/24] fix weird station layout issue Signed-off-by: Leonard Hilfrich --- .../export/station_switch_updates.py | 31 ++++++++++++++-- .../postprocess/apply_asset_topo_powsybl.py | 36 +++++++++++++------ .../asset_topology/asset_topology.py | 28 ++------------- .../asset_topology/materialized_topology.py | 17 --------- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py index b92c21ea5..54a7debd7 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/export/station_switch_updates.py @@ -16,7 +16,7 @@ import pandera as pa import pandera.typing as pat from toop_engine_interfaces.asset_topology.asset_topology import Topology -from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema @@ -26,6 +26,31 @@ def _get_busbar_lookup(station: MaterializedStation) -> dict[int, str]: return {index: busbar.grid_model_id for index, busbar in enumerate(station.busbars)} +def _get_asset_busbar_lookup( + station: MaterializedStation, + asset_connection: MaterializedAssetConnection, +) -> dict[int, str]: + """Resolve row-to-busbar ids for one asset connection. + + Simplified split-station actions can use per-asset logical rows that no longer match the + station-level busbar ids. When that happens, the asset bay selector-switch keys preserve the + physical busbar ordering needed to translate row changes back into switch updates. + """ + station_busbar_lookup = _get_busbar_lookup(station) + asset_bay = asset_connection.asset_bay + if asset_bay is None: + return station_busbar_lookup + + station_busbar_ids = list(station_busbar_lookup.values()) + asset_busbar_ids = list(asset_bay.sr_switch_grid_model_id.keys()) + if len(asset_busbar_ids) == len(station_busbar_ids) and not set(station_busbar_ids).issubset( + asset_bay.sr_switch_grid_model_id + ): + return {index: busbar_id for index, busbar_id in enumerate(asset_busbar_ids)} + + return station_busbar_lookup + + def _resolve_changed_stations( changed_stations: list[MaterializedStation], starting_topology: Topology, @@ -168,11 +193,11 @@ def _get_branch_switch_diffs( f"{changed_station.grid_model_id}. Use ActionSet.simplified_starting_topology as input." ) - changed_busbar_lookup = _get_busbar_lookup(changed_station) switching_xor = np.logical_xor(starting_station.branch_switching_table, changed_station.branch_switching_table) diff_switches: list[dict[str, str | bool]] = [] for column, changed_asset_connection in enumerate(changed_station.branch_connections): + changed_busbar_lookup = _get_asset_busbar_lookup(changed_station, changed_asset_connection) asset_bay = changed_asset_connection.asset_bay if asset_bay is None: continue @@ -248,7 +273,6 @@ def _get_injection_switch_diffs( f"{changed_station.grid_model_id}. Use ActionSet.simplified_starting_topology as input." ) - changed_busbar_lookup = _get_busbar_lookup(changed_station) switching_xor = np.logical_xor( starting_station.injection_switching_table, changed_station.injection_switching_table, @@ -256,6 +280,7 @@ def _get_injection_switch_diffs( diff_switches: list[dict[str, str | bool]] = [] for column, changed_asset_connection in enumerate(changed_station.injection_connections): + changed_busbar_lookup = _get_asset_busbar_lookup(changed_station, changed_asset_connection) asset_bay = changed_asset_connection.asset_bay if asset_bay is None: continue diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index 71614cba4..6f2513b00 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -34,7 +34,7 @@ ) from toop_engine_interfaces.asset_topology.asset_topology_helpers import accumulate_diffs from toop_engine_interfaces.asset_topology.assets import BusbarCoupler -from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedStation +from toop_engine_interfaces.asset_topology.materialized_topology import MaterializedAssetConnection, MaterializedStation from toop_engine_interfaces.interface_helpers import get_empty_dataframe_from_model from toop_engine_interfaces.switch_update_schema import SwitchUpdateSchema @@ -88,9 +88,31 @@ def get_busbar_lookup(station: MaterializedStation) -> dict[int, str]: return {index: busbar.grid_model_id for index, busbar in enumerate(station.busbars)} +def _get_asset_busbar_lookup( + station: MaterializedStation, + asset_connection: MaterializedAssetConnection, +) -> dict[int, str]: + """Resolve row-to-busbar ids for one asset connection. + + Simplified split-station actions can compress station rows while keeping per-asset selector + switch ids that refer to the original physical busbars. In that case the row semantics are + asset-local and must be recovered from the asset bay ordering instead of the station busbar ids. + """ + station_busbar_lookup = get_busbar_lookup(station) + asset_switch_ids = asset_connection.get_sr_switch() + if asset_switch_ids is None: + return station_busbar_lookup + + station_busbar_ids = list(station_busbar_lookup.values()) + asset_busbar_ids = list(asset_switch_ids.keys()) + if len(asset_busbar_ids) == len(station_busbar_ids) and not set(station_busbar_ids).issubset(asset_switch_ids): + return {index: busbar_id for index, busbar_id in enumerate(asset_busbar_ids)} + + return station_busbar_lookup + + def _get_branch_switch_states_from_station( station: MaterializedStation, - busbar_id_dict: dict[int, str], ) -> tuple[list[dict[str, str | bool]], list[dict[str, str | bool]]]: """Translate branch selector and breaker states of one station to switch updates. @@ -98,8 +120,6 @@ def _get_branch_switch_states_from_station( ---------- station : MaterializedStation Station whose branch-side switching state should be exported. - busbar_id_dict : dict[int, str] - Mapping from branch switching-table row index to busbar id. Returns ------- @@ -122,6 +142,7 @@ def _get_branch_switch_states_from_station( ) for column, asset_connection in enumerate(station.branch_connections): + busbar_id_dict = _get_asset_busbar_lookup(station, asset_connection) asset_switch_ids = asset_connection.get_sr_switch() if asset_switch_ids is None: continue @@ -155,7 +176,6 @@ def _get_branch_switch_states_from_station( def _get_injection_switch_states_from_station( station: MaterializedStation, - busbar_id_dict: dict[int, str], ) -> tuple[list[dict[str, str | bool]], list[dict[str, str | bool]]]: """Translate injection selector and breaker states of one station to switch updates. @@ -163,8 +183,6 @@ def _get_injection_switch_states_from_station( ---------- station : MaterializedStation Station whose injection-side switching state should be exported. - busbar_id_dict : dict[int, str] - Mapping from injection switching-table row index to busbar id. Returns ------- @@ -187,6 +205,7 @@ def _get_injection_switch_states_from_station( ) for column, asset_connection in enumerate(station.injection_connections): + busbar_id_dict = _get_asset_busbar_lookup(station, asset_connection) asset_switch_ids = asset_connection.get_sr_switch() if asset_switch_ids is None: continue @@ -236,14 +255,11 @@ def get_asset_switch_states_from_station( Two dataframes containing reassignment-related switch updates and disconnection-related switch updates. """ - busbar_id_dict = get_busbar_lookup(station) branch_reassignment_list, branch_disconnection_list = _get_branch_switch_states_from_station( station=station, - busbar_id_dict=busbar_id_dict, ) injection_reassignment_list, injection_disconnection_list = _get_injection_switch_states_from_station( station=station, - busbar_id_dict=busbar_id_dict, ) switch_reassignment_list = [*branch_reassignment_list, *injection_reassignment_list] switch_disconnection_list = [*branch_disconnection_list, *injection_disconnection_list] diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py index b8e7ce382..f0cd8ca05 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/asset_topology.py @@ -169,8 +169,6 @@ def materialize_stations(self) -> list[MaterializedStation]: materialized_stations: list[MaterializedStation] = [] for station in self.raw_stations: - station_busbar_ids = {busbar.grid_model_id for busbar in station.busbars} - station_branch_assets = [ branch_asset_map[asset_connection.asset_id].model_copy(deep=True) for asset_connection in station.branch_connections @@ -181,35 +179,13 @@ def materialize_stations(self) -> list[MaterializedStation]: ] station_branch_asset_bays = [ - asset_bay_map[asset_connection.asset_bay_id].model_copy( - update={ - "sr_switch_grid_model_id": { - busbar_id: switch_id - for busbar_id, switch_id in asset_bay_map[ - asset_connection.asset_bay_id - ].sr_switch_grid_model_id.items() - if busbar_id in station_busbar_ids - } - }, - deep=True, - ) + asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) if asset_connection.asset_bay_id is not None else None for asset_connection in station.branch_connections ] station_injection_asset_bays = [ - asset_bay_map[asset_connection.asset_bay_id].model_copy( - update={ - "sr_switch_grid_model_id": { - busbar_id: switch_id - for busbar_id, switch_id in asset_bay_map[ - asset_connection.asset_bay_id - ].sr_switch_grid_model_id.items() - if busbar_id in station_busbar_ids - } - }, - deep=True, - ) + asset_bay_map[asset_connection.asset_bay_id].model_copy(deep=True) if asset_connection.asset_bay_id is not None else None for asset_connection in station.injection_connections diff --git a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py index 7e341f087..4cf038d0a 100644 --- a/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py +++ b/packages/interfaces_pkg/src/toop_engine_interfaces/asset_topology/materialized_topology.py @@ -96,23 +96,6 @@ def check_asset_shapes(self: "MaterializedStation") -> "MaterializedStation": ) return self - @model_validator(mode="after") - def check_asset_bay(self: "MaterializedStation") -> "MaterializedStation": - """Check if the asset bay bus is in busbars.""" - busbar_grid_model_id = [busbar.grid_model_id for busbar in self.busbars] - for asset_connection in [*self.branch_connections, *self.injection_connections]: - asset = asset_connection.asset - asset_bay = asset_connection.asset_bay - if asset_bay is not None: - for busbar_id in asset_bay.sr_switch_grid_model_id.keys(): - if busbar_id not in busbar_grid_model_id: - raise ValueError( - f"busbar_id {busbar_id} in asset {asset.grid_model_id} does not exist in busbars" - f" Station_id: {self.grid_model_id}, Name: {self.name}" - ) - - return self - def __eq__(self, other: object) -> bool: """Check if two stations are equal.