Skip to content

Commit

Permalink
New tool for finding nodes by fuzzy search
Browse files Browse the repository at this point in the history
  • Loading branch information
aloytag committed Aug 23, 2023
1 parent 106a74e commit 18170b0
Show file tree
Hide file tree
Showing 29 changed files with 1,276 additions and 8 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The EGS main window is organized as follows:
* The main work area can display either the **Graph** or the **Data model**. The **Data model** is shown as a set of tables arranged in tabs. For more information about this data (including the meaning of column names), see the [pandapower documentation](https://pandapower.readthedocs.io/).
* The side toolbar lists the supported components. An element is added to the **Graph** by clicking on the corresponding icon. In some cases, an icon may represent a category (e.g., loads). In such cases, a dialog allows you to choose the required type within that category. For example, in the loads category, six different types are available.
Switches work in a different way. According to ```pandapower```, switches can be added between two buses or between a bus and a line (AC line) or transformer. Thus, if you want to do the first, just select two buses and then click the switch button. On the contrary, if you want to add a switch next to a line (or transformer), select only the corresponding element and then click the switch button. In this case, a new dialog will allow you to select the bus.
* The upper toolbar is divided into three parts. The left part contains the file functions and the calculation options. Here it is possible to open/save files, export only the **Data model** to JSON, or simply delete the network and start a new one. The *"play"* button opens the dialog for a power flow calculation. Meanwhile, the right side gives access to the basic network configuration (name, base system power and rated frequency) and to the application settings dialog. The extension manager is displayed at the center. In order to run an extensión, just select one of them and click the run button.
* The upper toolbar is divided into three parts. The left part contains the file functions and the calculation options. Here it is possible to open/save files, export only the **Data model** to JSON, or simply delete the network and start a new one. The *"play"* button opens the dialog for a power flow calculation. Meanwhile, the right side gives access to the basic network configuration (name, base system power and rated frequency) and to the application settings dialog. The fuuzy search launcher and the extension manager are displayed at the center. In order to run an extensión, just select one of them and click the run button.
* The status bar at the bottom will notify when the grid has been modified and has not been saved.
* The menu bar includes most of the options available in the toolbars.

Expand Down Expand Up @@ -228,6 +228,7 @@ Settings are classified into four categories:
| ```H``` | Change selected nodes to the horizontal layout |
| ```Shift+V``` | Apply a vertical alignment to selected nodes |
| ```Shift+H``` | Apply a horizontal alignment to selected nodes |
| ```Ctrl+F``` | Find a node by fuzzy search |

> __Note__ <br>
> * ```Undo``` and ```Redo``` only work for simple actions.
Expand All @@ -236,8 +237,10 @@ Settings are classified into four categories:
> * For selecting nodes in the **Graph** you can use the left mouse button (```LMB```). Just click with the ```LMB```, hold and drag to mark the selection area.
> * Clicking with the ```LMB``` on the background will unselect all.
> * You can use ```Shift+LMB``` on a node in order to add it to the selection.
> * ```Ctrl+LMB``` on a selected node will unselect it.
> * The mouse scroll wheel can be used to zoom in and out.
> * Use the medium mouse button (```MMB```) to scroll over the **Graph**. Just click with the ```MMB```, hold and move.
> * Hold ```Shift+Alt+LMB``` to slice several connections (see the picture below).
If you prefer to use the mouse instead of keyboard shortcuts, just right click on the **Graph** and access a context menu that complements the upper toolbar and the menu bar.

Expand All @@ -248,7 +251,21 @@ If you prefer to use the mouse instead of keyboard shortcuts, just right click o
</p>
</br>

![EGS with dark theme](img/14_Context_menu.png)
<p align = "center">
<img src="./img/slice_connections.png" alt="Slicing connections" width="450">
<p align = "center">
<i>Slicing connections</i>
</p>
</br>

<p align = "center">
<img src="./img/fuzzy_search2.png" alt="Fuzzy search" width="350">
<p align = "center">
<i>Fuzzy search</i>
</p>
</br>

![Context menu](img/14_Context_menu.png)
<p align = "center">
<i>Context menu</i>
</p>
Expand Down
Binary file modified img/14_Context_menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
932 changes: 932 additions & 0 deletions img/14_Context_menu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/1_Main_Window.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/1_Main_Window_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/2_Dialogs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/4_Pandapower_DataFrames.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/fuzzy_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/fuzzy_search2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/slice_connections.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "electricalsim"
version = "0.0.7.5"
version = "0.0.7.6"
author = "Dr. Ing. Ariel S. Loyarte"
authors = [
{ name="Dr. Ing. Ariel S. Loyarte", email="[email protected]" },
Expand All @@ -16,7 +16,7 @@ dependencies = [
"PySide2>=5.15",
"Qt.py>=1.3.7",
"pandapower>=2.13.1",
"numba>=0.56.4",
"numba==0.56.4",
"pyqtdarktheme>=2.1.0",
"qtawesome>=1.2.2",
"pynput>=1.7.6",
Expand All @@ -26,7 +26,8 @@ dependencies = [
"qtpy>=2.3.0",
"pyshortcuts>=1.8.3",
"importlib_metadata>=4.11.3",
"NodeGraphQt>=0.6.17",
"NodeGraphQt>=0.6.21",
"fuzzysearch>=0.7.3",
]
classifiers = [
"Programming Language :: Python :: 3",
Expand Down
9 changes: 9 additions & 0 deletions src/electricalsim/Electrical_Grid_Simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,15 @@ def main():

main_window.layout_upper_toolbar.addStretch()

search_node_btn = QtWidgets.QToolButton(main_window)
search_node_btn.setToolTip('Find node (Ctrl+F)')
search_node_btn.setIconSize(icon_size)
search_node_btn.setIcon(qta.icon('mdi6.map-search-outline'))
main_window.layout_upper_toolbar.addWidget(search_node_btn)
search_node_btn.clicked.connect(graph.search_node)

main_window.layout_upper_toolbar.addWidget(QVLine()) # separator

extensions_label = QtWidgets.QLabel(main_window)
extensions_label.setText('Extensions:')
extensions_combobox = QtWidgets.QComboBox(main_window)
Expand Down
Binary file not shown.
Binary file modified src/electricalsim/__pycache__/version.cpython-39.pyc
Binary file not shown.
Binary file not shown.
7 changes: 7 additions & 0 deletions src/electricalsim/hotkeys/hotkey_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,10 @@ def duplicate_nodes(graph):
in_service=graph.net.bus.at[node.get_property('bus_index'), 'in_service'],
geodata=node_duplicated.pos())
node_duplicated.set_property('bus_index', bus_index, push_undo=False)


def find_node(graph):
"""
Shows the dialog for searching nodes.
"""
graph.search_node()
10 changes: 10 additions & 0 deletions src/electricalsim/hotkeys/hotkeys.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,15 @@
"shortcut":"Shift+H"
}
]
},
{
"type":"separator"
},
{
"type":"command",
"label":"Find node",
"file":"./hotkeys/hotkey_functions.py",
"function_name":"find_node",
"shortcut":"Ctrl+F"
}
]
Binary file modified src/electricalsim/lib/__pycache__/auxiliary.cpython-39.pyc
Binary file not shown.
Binary file modified src/electricalsim/lib/__pycache__/electricalGraph.cpython-39.pyc
Binary file not shown.
Binary file modified src/electricalsim/lib/__pycache__/table_widget.cpython-39.pyc
Binary file not shown.
41 changes: 41 additions & 0 deletions src/electricalsim/lib/auxiliary.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import configparser
import re

from Qt import QtWidgets
import qtawesome as qta
Expand All @@ -11,6 +12,46 @@
directory = os.path.dirname(__file__)
root_dir, _ = os.path.split(directory)

icon_for_type = {'BusNode.BusNode': 'ph.git-commit',
'LineNode.LineNode': 'ph.line-segment',
'StdLineNode.StdLineNode': 'ph.line-segment',
'DCLineNode.DCLineNode': 'ph.line-segment',
'ImpedanceNode.ImpedanceNode': 'mdi6.alpha-z-box-outline',
'TrafoNode.TrafoNode': 'ph.intersect',
'StdTrafoNode.StdTrafoNode': 'ph.intersect',
'Trafo3wNode.Trafo3wNode': 'ph.intersect',
'StdTrafo3wNode.StdTrafo3wNode': 'ph.intersect',
'GenNode.GenNode': 'mdi6.alpha-g-circle-outline',
'SGenNode.SGenNode': 'mdi6.alpha-g-circle-outline',
'ASGenNode.ASGenNode': 'mdi6.alpha-g-circle-outline',
'ExtGridNode.ExtGridNode': 'mdi6.grid',
'LoadNode.LoadNode': 'mdi6.download-circle-outline',
'ALoadNode.ALoadNode': 'mdi6.download-circle-outline',
'ShuntNode.ShuntNode': 'mdi6.download-circle-outline',
'MotorNode.MotorNode': 'mdi6.download-circle-outline',
'WardNode.WardNode': 'mdi6.download-circle-outline',
'XWardNode.XWardNode': 'mdi6.download-circle-outline',
'StorageNode.StorageNode': 'mdi6.battery-medium',
'SwitchNode.SwitchNode': 'mdi6.electric-switch'}


def natsort(s):
"""
Function key for natural sorting.
s: List with strings.
"""
return [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]


def natsort2(s):
"""
Function key for natural sorting.
s: List obtained as enumerate(list with strings).
"""
return [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s[1])]


def show_WIP(window_parent):
"""
Expand Down
22 changes: 21 additions & 1 deletion src/electricalsim/lib/electricalGraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
ward_dialog, xward_dialog, storage_dialog,
choose_bus_switch_dialog, switch_dialog,
network_settings_dialog, Settings_Dialog,
connecting_buses_dialog)
connecting_buses_dialog, search_node_dialog)
from extensions.extension_classes import ExtensionWorker


Expand Down Expand Up @@ -801,6 +801,7 @@ def add_trafo(self, **kwargs):
node.set_input(0, node_from.output(0), push_undo=False)
node.set_output(0, node_to.input(0), push_undo=False)
return node

def add_generator(self, **kwargs):
"""
Adds a generator to the graph: voltage-controlled gen.,
Expand Down Expand Up @@ -4210,3 +4211,22 @@ def update_widgets_properties(self):
except KeyError:
node.p_mw_widget.get_custom_widget().setValue(node.get_property('p_mw'))
node.q_mvar_widget.get_custom_widget().setValue(node.get_property('q_mvar'))

def search_node(self):
"""
Displays a dialog for searching nodes by name.
"""
all_nodes = self.all_nodes()

dialog = search_node_dialog(all_nodes)
dialog.setWindowIcon(QtGui.QIcon(icon_path))
main_win_rect = self.main_window.geometry()
dialog.move(main_win_rect.center() - dialog.rect().center()) # centering in the main window
if dialog.exec():
node = self.get_node_by_name(dialog.selected_node)
if node is not None:
self.clear_selection()
node.set_selected(True)
self.fit_to_selection()
self.main_window.toolBox.setCurrentIndex(0)
simulate_ESC_key()
2 changes: 1 addition & 1 deletion src/electricalsim/lib/table_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ def show_component(self):
"""
Show the selected component in the graph.
"""
self.graph.clear_selection()
node_name = self.model.get_data().iloc[self.row, :]['name']
node = self.graph.get_node_by_name(node_name)
if node is not None:
self.graph.clear_selection()
node.set_selected(True)
self.graph.fit_to_selection()
self.graph.main_window.toolBox.setCurrentIndex(0)
Expand Down
Binary file modified src/electricalsim/ui/__pycache__/dialogs.cpython-39.pyc
Binary file not shown.
108 changes: 108 additions & 0 deletions src/electricalsim/ui/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
from matplotlib.ticker import MaxNLocator
from matplotlib import rc

from fuzzysearch import find_near_matches

from lib.table_widget import TableWidget
from lib.auxiliary import icon_for_type, natsort2
from version import VERSION, DATE, AUTHOR, CONTACT

directory = os.path.dirname(__file__)
Expand Down Expand Up @@ -723,6 +726,111 @@ def switch():
return dialog


def search_node_dialog(all_nodes):
"""
all_nodes: List of all nodes in the graph.
Returns a dialog for searching nodes.
"""
names = [node.name() for node in all_nodes]
types = [node.type_ for node in all_nodes]

ordered = sorted(enumerate(names), key=natsort2)
all_names = []
all_types = []
for order, name in ordered:
all_names.append(name)
all_types.append(types[order])

ui_file = os.path.join(directory, 'search_node_dialog.ui')
dialog = QtCompat.loadUi(uifile=ui_file)
dialog.setModal(True)
dialog.setWindowFlags(QtCore.Qt.FramelessWindowHint)
dialog.selected_node = None # Name of the selected element (node)

dialog.btn_close.setIcon(qta.icon('mdi6.close'))

dialog.setStyleSheet('font-size: 20px')
dialog.label.setStyleSheet('font-size: 16px')
dialog.input_search.setFocus()
# dialog.frame.setStyleSheet('border: 1px solid #d3d3d3')

class SaveNodeName:
def __init__(self, name):
self.name = name

def __call__(self):
dialog.selected_node = self.name
dialog.accept()

def list_all_nodes():
widget = QtWidgets.QWidget()
dialog.vbox = QtWidgets.QVBoxLayout()
icon_size = QtCore.QSize(32, 32)

for node_name, node_type in zip(all_names, all_types):
hbox = QtWidgets.QHBoxLayout()
widget_btn = QtWidgets.QWidget()
btn_node = QtWidgets.QToolButton(dialog)
btn_node.setIcon(qta.icon(icon_for_type[node_type]))
btn_node.setText(node_name)
btn_node.setIconSize(icon_size)
btn_node.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
btn_node.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
btn_action = SaveNodeName(node_name)
btn_node.clicked.connect(btn_action)
hbox.addWidget(btn_node)
widget_btn.setLayout(hbox)
dialog.vbox.addWidget(widget_btn)
# dialog.scrollArea.setFocusProxy(widget_btn)

dialog.vbox.addStretch()
widget.setLayout(dialog.vbox)
dialog.scrollArea.setWidget(widget)

list_all_nodes()

# dialog.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
dialog.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
dialog.scrollArea.setWidgetResizable(True)

def nodes_found(text):
dialog.scrollArea.takeWidget() # Removing list...
if text=='':
list_all_nodes()
return

widget = QtWidgets.QWidget()
dialog.vbox = QtWidgets.QVBoxLayout()
icon_size = QtCore.QSize(32, 32)
for node_name, node_type in zip(all_names, all_types):
search_text = node_name.join((node_name.lower(), node_name.upper()))
found_list = find_near_matches(text, search_text, max_l_dist=1)
if text in search_text or (found_list and not all([m.matched=='' for m in found_list])):
hbox = QtWidgets.QHBoxLayout()
widget_btn = QtWidgets.QWidget()
btn_node = QtWidgets.QToolButton(dialog)
btn_node.setIcon(qta.icon(icon_for_type[node_type]))
btn_node.setText(node_name)
btn_node.setIconSize(icon_size)
btn_node.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
btn_node.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
btn_action = SaveNodeName(node_name)
btn_node.clicked.connect(btn_action)
hbox.addWidget(btn_node)
widget_btn.setLayout(hbox)
dialog.vbox.addWidget(widget_btn)

dialog.vbox.addStretch()
widget.setLayout(dialog.vbox)
dialog.scrollArea.setWidget(widget)


dialog.input_search.textChanged.connect(nodes_found)

return dialog


class Toolbar_Matplotlib_custom(NavigationToolbar):
"""
Custom Matplotlib toolbar.
Expand Down
Loading

0 comments on commit 18170b0

Please sign in to comment.