From ef08cac4beb7d04e86e8e0d81d420a17422d08de Mon Sep 17 00:00:00 2001 From: Christian Biasuzzi Date: Wed, 24 Jul 2024 16:08:16 +0200 Subject: [PATCH] Adds a use_name parameter to the network explorer (#15) * uses use_name to display ids or names in the selection list and in the tabs content * introduces SelectionContext class to handle it Signed-off-by: Christian Biasuzzi --- docs/user_guide/network_explorer.md | 3 +- src/pypowsybl_jupyter/networkexplorer.py | 70 ++++++++++++------------ src/pypowsybl_jupyter/selectcontext.py | 56 +++++++++++++++++++ 3 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 src/pypowsybl_jupyter/selectcontext.py diff --git a/docs/user_guide/network_explorer.md b/docs/user_guide/network_explorer.md index cbc2a0f..7df112c 100644 --- a/docs/user_guide/network_explorer.md +++ b/docs/user_guide/network_explorer.md @@ -37,10 +37,11 @@ Switches can also be clicked, causing their status in the network to change; Ple Other than the target network, the Network explorer can be customized using additional parameters: ```python -network_explorer(network: Network, vl_id : str = None, depth: int = 0, high_nominal_voltage_bound: float = -1, low_nominal_voltage_bound: float = -1, nad_parameters: NadParameters = None, sld_parameters: SldParameters = None) +network_explorer(network: Network, vl_id : str = None, use_name:bool = True, depth: int = 0, high_nominal_voltage_bound: float = -1, low_nominal_voltage_bound: float = -1, nad_parameters: NadParameters = None, sld_parameters: SldParameters = None): ``` - vl_id: the starting VL to display. If None, display the first VL from network.get_voltage_levels() +- use_name: when available, display VLs names instead of their ids (default is to use names) - depth: the diagram depth around the voltage level, controls the size of the sub network. In the SLD tab will be always displayed one diagram, from the VL list currently selected item. - low_nominal_voltage_bound: low bound to filter voltage level according to nominal voltage - high_nominal_voltage_bound: high bound to filter voltage level according to nominal voltage diff --git a/src/pypowsybl_jupyter/networkexplorer.py b/src/pypowsybl_jupyter/networkexplorer.py index 2f8a92b..f19e149 100644 --- a/src/pypowsybl_jupyter/networkexplorer.py +++ b/src/pypowsybl_jupyter/networkexplorer.py @@ -8,16 +8,20 @@ from pypowsybl.network import Network, NadParameters, SldParameters from .nadwidget import display_nad, update_nad from .sldwidget import display_sld, update_sld +from .selectcontext import SelectContext import ipywidgets as widgets -def network_explorer(network: Network, vl_id : str = None, depth: int = 0, high_nominal_voltage_bound: float = -1, low_nominal_voltage_bound: float = -1, nad_parameters: NadParameters = None, sld_parameters: SldParameters = None): +def network_explorer(network: Network, vl_id : str = None, use_name:bool = True, depth: int = 0, + high_nominal_voltage_bound: float = -1, low_nominal_voltage_bound: float = -1, + nad_parameters: NadParameters = None, sld_parameters: SldParameters = None): """ Creates a combined NAD and SLD explorer widget for the network. Diagrams are displayed on two different tabs. Args: network: the input network vl_id: the starting VL to display. If None, display the first VL from network.get_voltage_levels() + use_name: when available, display VLs names instead of their ids (default is to use names) depth: the diagram depth around the voltage level, controls the size of the sub network. In the SLD tab will be always displayed one diagram, from the VL list currently selected item. low_nominal_voltage_bound: low bound to filter voltage level according to nominal voltage high_nominal_voltage_bound: high bound to filter voltage level according to nominal voltage @@ -31,18 +35,15 @@ def network_explorer(network: Network, vl_id : str = None, depth: int = 0, high_ network_explorer(pp.network.create_eurostag_tutorial_example1_network()) """ - vls = network.get_voltage_levels(attributes=[]) + sel_ctx=SelectContext(network, vl_id, use_name) + nad_widget=None sld_widget=None - selected_vl = vls.index[0] if vl_id is None else vl_id - if selected_vl not in vls.index: - raise ValueError(f'a voltage level {vl_id} does not exist in the network.') - selected_depth=depth npars = nad_parameters if nad_parameters is not None else NadParameters(edge_name_displayed=False, - id_displayed=False, + id_displayed=not use_name, edge_info_along_edge=False, power_value_precision=1, angle_value_precision=0, @@ -51,17 +52,15 @@ def network_explorer(network: Network, vl_id : str = None, depth: int = 0, high_ bus_legend=False, substation_description_displayed=True) - spars=sld_parameters if sld_parameters is not None else SldParameters(use_name=True) + spars=sld_parameters if sld_parameters is not None else SldParameters(use_name=use_name, nodes_infos=True) def go_to_vl(event: any): - nonlocal selected_vl arrow_vl= str(event.clicked_nextvl) - vl_filtered_list=list(found.options) - if arrow_vl not in vl_filtered_list: - vl_filtered_list.append(arrow_vl) - found.options=vl_filtered_list - selected_vl=arrow_vl - found.value=arrow_vl + sel_ctx.extend_filtered_vls(arrow_vl) + found.value=None + found.options=sel_ctx.get_filtered_vls_as_list() + sel_ctx.set_selected(arrow_vl) + found.value=sel_ctx.get_selected() def toggle_switch(event: any): idswitch = event.clicked_switch.get('id') @@ -70,11 +69,12 @@ def toggle_switch(event: any): update_sld_diagram(True) update_nad_diagram() - def update_nad_diagram(): nonlocal nad_widget - if len(selected_vl)>0: - new_diagram_data=network.get_network_area_diagram(voltage_level_ids=selected_vl, depth=selected_depth, high_nominal_voltage_bound=high_nominal_voltage_bound, low_nominal_voltage_bound=low_nominal_voltage_bound, nad_parameters=npars) + if sel_ctx.get_selected() is not None: + new_diagram_data=network.get_network_area_diagram(voltage_level_ids=sel_ctx.get_selected(), + depth=selected_depth, high_nominal_voltage_bound=high_nominal_voltage_bound, + low_nominal_voltage_bound=low_nominal_voltage_bound, nad_parameters=npars) if nad_widget==None: nad_widget=display_nad(new_diagram_data) else: @@ -82,8 +82,8 @@ def update_nad_diagram(): def update_sld_diagram(kv: bool = False): nonlocal sld_widget - if selected_vl is not None: - sld_diagram_data=network.get_single_line_diagram(selected_vl, spars) + if sel_ctx.get_selected() is not None: + sld_diagram_data=network.get_single_line_diagram(sel_ctx.get_selected(), spars) if sld_widget==None: sld_widget=display_sld(sld_diagram_data, enable_callbacks=True) sld_widget.on_nextvl(lambda event: go_to_vl(event)) @@ -92,8 +92,8 @@ def update_sld_diagram(kv: bool = False): else: update_sld(sld_widget, sld_diagram_data, keep_viewbox=kv, enable_callbacks=True) - - nadslider = widgets.IntSlider(value=selected_depth, min=0, max=20, step=1, description='depth:', disabled=False, continuous_update=False, orientation='horizontal', readout=True, readout_format='d') + nadslider = widgets.IntSlider(value=selected_depth, min=0, max=20, step=1, description='depth:', disabled=False, + continuous_update=False, orientation='horizontal', readout=True, readout_format='d') def on_nadslider_changed(d): nonlocal selected_depth @@ -104,34 +104,34 @@ def on_nadslider_changed(d): vl_input = widgets.Text( value='', - placeholder='Voltage level ID', + placeholder='Voltage level Name' if use_name else 'Voltage level Id', description='Filter', disabled=False, - continuous_update=True + continuous_update=True, + layout=widgets.Layout(width='350px') ) - + def on_text_changed(d): nonlocal found - if d['new'] != '': - found.options = vls[vls.index.str.contains(d['new'], regex=False)].index - else: - found.options=list(vls.index) - found.value=selected_vl + sel_ctx.apply_filter(d['new']) + found.value=None + found.options = sel_ctx.get_filtered_vls_as_list() + if sel_ctx.is_selected_in_filtered_vls(): + found.value=sel_ctx.get_selected() vl_input.observe(on_text_changed, names='value') found = widgets.Select( - options=list(vls.index), - value=selected_vl, + options=sel_ctx.get_filtered_vls_as_list(), + value=sel_ctx.get_selected(), description='Found', disabled=False, - layout=widgets.Layout(height='670px') + layout=widgets.Layout(width='350px', height='670px') ) def on_selected(d): - nonlocal selected_vl if d['new'] != None: - selected_vl=d['new'] + sel_ctx.set_selected(d['new']) update_nad_diagram() update_sld_diagram() diff --git a/src/pypowsybl_jupyter/selectcontext.py b/src/pypowsybl_jupyter/selectcontext.py new file mode 100644 index 0000000..9e5b4c9 --- /dev/null +++ b/src/pypowsybl_jupyter/selectcontext.py @@ -0,0 +1,56 @@ +# Copyright (c) 2024, RTE (http://www.rte-france.com) +# 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 http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# + +from collections import OrderedDict +import pandas as pd +from pypowsybl.network import Network + +class SelectContext: + + def __init__(self, network:Network = None, vl_id : str = None, use_name:bool = True): + self.network = network + self.use_name = use_name + self.display_attribute = 'name' if use_name else 'id' + + self.vls = network.get_voltage_levels(attributes=['name']) + self.vls['name'] = self.vls['name'].replace('', pd.NA).fillna(self.vls.index.to_series().astype(str)) + self.vls['id'] = self.vls.index + + self.vls = self.vls.sort_values(by=self.display_attribute) if use_name else self.vls.sort_index() + + self.apply_filter(None) + + self.set_selected(self.vls.index[0] if vl_id is None else vl_id) + + def get_vls(self): + return self.vls + + def set_selected(self, id): + if id in self.vls.index: + self.selected_vl = id + else: + raise ValueError(f'a voltage level with id={id} does not exist in the network.') + + def get_selected(self): + return self.selected_vl + + def apply_filter(self, sfilter, search_attribute = None): + if sfilter is not None and sfilter != '': + search_by = self.display_attribute if search_attribute is None else search_attribute + self.vls_filtered = self.vls[self.vls[search_by].str.contains(sfilter, case=False, na=False, regex=False)] + else: + self.vls_filtered = self.vls + + def is_selected_in_filtered_vls(self): + return self.selected_vl in self.vls_filtered.index + + def get_filtered_vls_as_list(self): + return list(zip(self.vls_filtered[self.display_attribute].values.tolist(), self.vls_filtered.index)) + + def extend_filtered_vls(self, id): + if (id in self.vls.index) and (id not in self.vls_filtered.index): + self.vls_filtered = pd.concat([self.vls_filtered, self.vls.loc[[id]]])