diff --git a/requirements.txt b/requirements.txt index 7043dbe93..eed2f984a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ networkx>=2.1 pandas>=1.1 xlwt>=1.3.0 xlrd>=1.1.0 -ortools>=9.8.0, <=9.9.3963 +ortools>=9.10.0, matplotlib>=2.1.1 qtconsole>=4.3.1 openpyxl>=2.4.9 diff --git a/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_container.py b/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_container.py index b20bfb09c..610b85cbb 100644 --- a/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_container.py +++ b/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_container.py @@ -122,7 +122,7 @@ def add_segment(self, segment: MapLineSegment): """ self.segments_list.append(segment) - def set_colour(self, color: QColor, w, style: Qt.PenStyle, tool_tip: str = '') -> None: + def set_colour(self, color: QColor, style: Qt.PenStyle, tool_tip: str = '') -> None: """ Set color and style :param color: QColor instance @@ -133,7 +133,7 @@ def set_colour(self, color: QColor, w, style: Qt.PenStyle, tool_tip: str = '') - """ for segment in self.segments_list: segment.setToolTip(tool_tip) - segment.set_colour(color=color, w=w, style=style) + segment.set_colour(color=color, style=style) def update_connectors(self) -> None: """ diff --git a/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_segment.py b/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_segment.py index c5ed83920..cd32815ff 100644 --- a/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_segment.py +++ b/src/GridCal/Gui/Diagrams/MapWidget/Branches/map_line_segment.py @@ -76,7 +76,7 @@ def __init__(self, first: NodeGraphicItem, second: NodeGraphicItem, container: M self.first.add_position_change_callback(self.set_from_side_coordinates) self.second.add_position_change_callback(self.set_to_side_coordinates) - self.set_colour(self.color, self.width, self.style) + self.set_colour(self.color, self.style) self.update_endings() self.needsUpdate = True self.setZValue(0) @@ -115,7 +115,7 @@ def set_width(self, width: float): self.arrow_p_to.label.setScale(width) self.arrow_q_to.label.setScale(width) - def set_colour(self, color: QColor, w: float, style: Qt.PenStyle): + def set_colour(self, color: QColor, style: Qt.PenStyle): """ Set color and style :param color: QColor instance @@ -124,14 +124,13 @@ def set_colour(self, color: QColor, w: float, style: Qt.PenStyle): :return: """ - pen = QPen(color, w, style, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin) - pen.setWidthF(w) + pen = QPen(color, self.width, style, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin) self.setPen(pen) - self.arrow_p_from.set_colour(color, w, style) - self.arrow_q_from.set_colour(color, w, style) - self.arrow_p_to.set_colour(color, w, style) - self.arrow_q_to.set_colour(color, w, style) + self.arrow_p_from.set_colour(color, self.arrow_p_from.w, style) + self.arrow_q_from.set_colour(color, self.arrow_q_from.w, style) + self.arrow_p_to.set_colour(color, self.arrow_p_to.w, style) + self.arrow_q_to.set_colour(color, self.arrow_q_to.w, style) def set_from_side_coordinates(self, x: float, y: float): """ @@ -286,7 +285,7 @@ def set_enable(self, val=True): self.color = OTHER['color'] # Set pen for everyone - self.set_colour(self.color, self.width, self.style) + self.set_colour(self.color, self.style) def enable_disable_label_drawing(self): """ diff --git a/src/GridCal/Gui/Diagrams/MapWidget/Substation/substation_graphic_item.py b/src/GridCal/Gui/Diagrams/MapWidget/Substation/substation_graphic_item.py index 499d2cdbc..6dfb0b454 100644 --- a/src/GridCal/Gui/Diagrams/MapWidget/Substation/substation_graphic_item.py +++ b/src/GridCal/Gui/Diagrams/MapWidget/Substation/substation_graphic_item.py @@ -84,9 +84,9 @@ def __init__(self, self.setAcceptHoverEvents(True) # Allow selecting the node - self.setFlag(self.GraphicsItemFlag.ItemIsSelectable | QGraphicsRectItem.ItemIsMovable) + self.setFlag(self.GraphicsItemFlag.ItemIsSelectable | self.GraphicsItemFlag.ItemIsMovable) - self.setCursor(QCursor(Qt.PointingHandCursor)) + self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # Create a pen with reduced line width self.change_pen_width(0.5) @@ -113,6 +113,7 @@ def set_size(self, r: float): rect = self.rect() rect.setWidth(r) rect.setHeight(r) + self.radius = r # change the width and height while keeping the same center r2 = r / 2 @@ -121,8 +122,8 @@ def set_size(self, r: float): # Set the new rectangle with the updated dimensions self.setRect(new_x, new_y, r, r) - self.radius = r + # update the callbacks position for the lines to move accordingly self.set_callabacks(new_x + r2, new_y + r2) for vl_graphics in self.voltage_level_graphics: @@ -130,7 +131,21 @@ def set_size(self, r: float): self.update_diagram() - def set_api_object_color(self): + def resize_voltage_levels(self): + """ + + :return: + """ + max_vl = 1.0 # 1 KV + for vl_graphics in self.voltage_level_graphics: + max_vl = max(max_vl, vl_graphics.api_object.Vnom) + + for vl_graphics in self.voltage_level_graphics: + # radius here is the width, therefore we need to send W/2 + scale = vl_graphics.api_object.Vnom / max_vl * 0.5 + vl_graphics.set_size(r=self.radius * scale) + + def set_api_object_color(self) -> None: """ Gather the API object color and update this objects """ @@ -169,12 +184,20 @@ def sort_voltage_levels(self) -> None: """ Set the Zorder based on the voltage level voltage """ - # TODO: Check this - sorted_objects = sorted(self.voltage_level_graphics, key=lambda x: x.api_object.Vnom) + max_vl = 1.0 # 1 KV + for vl_graphics in self.voltage_level_graphics: + max_vl = max(max_vl, vl_graphics.api_object.Vnom) + + for vl_graphics in self.voltage_level_graphics: + scale = vl_graphics.api_object.Vnom / max_vl * 0.8 + vl_graphics.set_size(r=self.radius * scale) + vl_graphics.center_on_substation() + + sorted_objects = sorted(self.voltage_level_graphics, key=lambda x: -x.api_object.Vnom) for i, vl_graphics in enumerate(sorted_objects): vl_graphics.setZValue(i) - def update_diagram(self): + def update_diagram(self) -> None: """ Updates the element position in the diagram (to save) :return: @@ -246,7 +269,7 @@ def hoverEnterEvent(self, event: QtWidgets.QGraphicsSceneHoverEvent) -> None: # self.editor.map.view.in_item = True self.set_color(self.hoover_color, self.color) self.hovered = True - QApplication.instance().setOverrideCursor(Qt.PointingHandCursor) + QApplication.instance().setOverrideCursor(Qt.CursorShape.PointingHandCursor) def hoverLeaveEvent(self, event: QtWidgets.QGraphicsSceneHoverEvent) -> None: """ @@ -266,34 +289,39 @@ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): add_menu_entry(menu=menu, text="Add voltage level", - icon_path="", + icon_path=":/Icons/icons/plus.svg", function_ptr=self.add_voltage_level) add_menu_entry(menu=menu, text="Create line from here", - icon_path="", + icon_path=":/Icons/icons/plus.svg", function_ptr=self.create_new_line) add_menu_entry(menu=menu, - text="Move to API coordinates", - icon_path="", + text="Set coordinates to DB", + icon_path=":/Icons/icons/down.svg", function_ptr=self.move_to_api_coordinates) add_menu_entry(menu=menu, - text="Remove", - icon_path="", + text="Remove from schematic", + icon_path=":/Icons/icons/delete_schematic.svg", function_ptr=self.remove_function) - add_menu_entry(menu=menu, - text="ADD node", - icon_path=":/Icons/icons/divide.svg", - function_ptr=self.add_function) + # add_menu_entry(menu=menu, + # text="ADD node", + # icon_path=":/Icons/icons/plus.svg", + # function_ptr=self.add_function) add_menu_entry(menu=menu, text="Show diagram", - icon_path="", + icon_path=":/Icons/icons/grid_icon.svg", function_ptr=self.new_substation_diagram) + add_menu_entry(menu=menu, + text="Plot", + icon_path=":/Icons/icons/plot.svg", + function_ptr=self.plot) + menu.exec_(event.screenPos()) def create_new_line(self): @@ -343,7 +371,7 @@ def remove_function(self) -> None: "Remove substation graphics") if ok: - self.editor.removeSubstation(self) + self.editor.removeSubstation(substation=self) def move_to_api_coordinates(self): """ @@ -364,6 +392,13 @@ def new_substation_diagram(self): """ self.editor.new_substation_diagram(substation=self.api_object) + def plot(self): + """ + Plot the substations data + """ + i = self.editor.circuit.get_substations().index(self.api_object) + self.editor.plot_substation(i, self.api_object) + def add_voltage_level(self) -> None: """ Add Voltage Level @@ -392,9 +427,8 @@ def add_voltage_level(self) -> None: self.editor.circuit.add_voltage_level(vl) self.editor.circuit.add_bus(obj=bus) - - self.editor.add_api_voltage_level(substation_graphics=self, - api_object=vl) + self.editor.add_api_voltage_level(substation_graphics=self, api_object=vl) + self.sort_voltage_levels() def set_color(self, inner_color: QColor = None, border_color: QColor = None) -> None: """ @@ -433,14 +467,6 @@ def getPos(self) -> QPointF: return center_point - # def resize(self, new_radius: float) -> None: - # """ - # Resize the node. - # :param new_radius: New radius for the node. - # """ - # self.radius = new_radius - # self.setRect(self.x - new_radius, self.y - new_radius, new_radius * 2, new_radius * 2) - def change_pen_width(self, width: float) -> None: """ Change the pen width for the node. diff --git a/src/GridCal/Gui/Diagrams/MapWidget/Substation/voltage_level_graphic_item.py b/src/GridCal/Gui/Diagrams/MapWidget/Substation/voltage_level_graphic_item.py index f0b46d266..93ba2ed65 100644 --- a/src/GridCal/Gui/Diagrams/MapWidget/Substation/voltage_level_graphic_item.py +++ b/src/GridCal/Gui/Diagrams/MapWidget/Substation/voltage_level_graphic_item.py @@ -26,7 +26,6 @@ from GridCalEngine.Devices.Substation.voltage_level import VoltageLevel from GridCalEngine.Devices.Substation.bus import Bus -from GridCalEngine.enumerations import DeviceType if TYPE_CHECKING: # Only imports the below statements during type checking from GridCal.Gui.Diagrams.MapWidget.grid_map_widget import GridMapWidget @@ -64,21 +63,25 @@ def __init__(self, api_object=api_object, editor=editor, draw_labels=draw_labels) - QGraphicsEllipseItem.__init__(self, parent_center.x(), parent_center.y(), r * api_object.Vnom * 0.01, - r * api_object.Vnom * 0.01, parent) + QGraphicsEllipseItem.__init__(self, + parent_center.x(), + parent_center.y(), + r * api_object.Vnom * 0.01, + r * api_object.Vnom * 0.01, + parent) parent.register_voltage_level(vl=self) self.editor: GridMapWidget = editor # to reinforce the type + self.api_object: VoltageLevel = api_object # to reinforce the type self.radius = r * api_object.Vnom * 0.01 - # print(f"VL created at x:{parent_center.x()}, y:{parent_center.y()}") self.setAcceptHoverEvents(True) # Enable hover events for the item - # self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable) # Allow moving the node - self.setFlag( - self.GraphicsItemFlag.ItemIsSelectable | self.GraphicsItemFlag.ItemIsMovable) # Allow selecting the node + + # Allow moving the node + self.setFlag(self.GraphicsItemFlag.ItemIsSelectable | self.GraphicsItemFlag.ItemIsMovable) # Create a pen with reduced line width self.change_pen_width(0.5) @@ -114,6 +117,26 @@ def move_to_xy(self, x: float, y: float): self.setRect(x, y, self.rect().width(), self.rect().height()) return x, y + def set_size(self, r: float): + """ + + :param r: radius in pixels + :return: + """ + # if r != self.radius: + rect = self.rect() + rect.setWidth(r) + rect.setHeight(r) + self.radius = r + + # change the width and height while keeping the same center + r2 = r / 2 + new_x = rect.x() - r2 + new_y = rect.y() - r2 + + # Set the new rectangle with the updated dimensions + self.setRect(new_x, new_y, r, r) + def updateDiagram(self) -> None: """ @@ -124,8 +147,6 @@ def updateDiagram(self) -> None: lat, long = self.editor.to_lat_lon(x=center_point.x() + real_position.x(), y=center_point.y() + real_position.y()) - print(f'Updating VL position id:{self.api_object.idtag}, lat:{lat}, lon:{long}') - self.editor.update_diagram_element(device=self.api_object, latitude=lat, longitude=long, diff --git a/src/GridCal/Gui/Diagrams/MapWidget/grid_map_widget.py b/src/GridCal/Gui/Diagrams/MapWidget/grid_map_widget.py index b4fc2acdc..7f292bfe5 100644 --- a/src/GridCal/Gui/Diagrams/MapWidget/grid_map_widget.py +++ b/src/GridCal/Gui/Diagrams/MapWidget/grid_map_widget.py @@ -20,6 +20,9 @@ import json import numpy as np import math +import pandas as pd +from matplotlib import pyplot as plt + from PySide6.QtWidgets import QGraphicsItem from collections.abc import Callable from PySide6.QtSvg import QSvgGenerator @@ -40,9 +43,11 @@ from GridCalEngine.Devices.Substation.voltage_level import VoltageLevel from GridCalEngine.Devices.Branches.line_locations import LineLocation from GridCalEngine.Devices.multi_circuit import MultiCircuit -from GridCalEngine.enumerations import DeviceType +from GridCalEngine.enumerations import DeviceType, ResultTypes from GridCalEngine.Devices.types import ALL_DEV_TYPES from GridCalEngine.basic_structures import Logger +from GridCalEngine.Simulations.OPF.opf_ts_results import OptimalPowerFlowTimeSeriesResults +from GridCalEngine.Simulations.PowerFlow.power_flow_ts_results import PowerFlowTimeSeriesResults from GridCal.Gui.Diagrams.MapWidget.Branches.map_ac_line import MapAcLine from GridCal.Gui.Diagrams.MapWidget.Branches.map_dc_line import MapDcLine @@ -57,7 +62,7 @@ import GridCalEngine.Devices.Diagrams.palettes as palettes from GridCal.Gui.Diagrams.graphics_manager import ALL_MAP_GRAPHICS from GridCal.Gui.Diagrams.MapWidget.Tiles.tiles import Tiles -from GridCal.Gui.Diagrams.base_diagram_widget import BaseDiagramWidget, qimage_to_cv +from GridCal.Gui.Diagrams.base_diagram_widget import BaseDiagramWidget from GridCal.Gui.messages import error_msg if TYPE_CHECKING: @@ -870,10 +875,9 @@ def wheelEvent(self, event: QWheelEvent): """ # SANTIAGO: NO TOCAR ESTO ES EL COMPORTAMIENTO DESEADO - self.update_device_sizes() - def update_device_sizes(self): + def get_branch_width(self): """ :return: @@ -882,6 +886,13 @@ def update_device_sizes(self): min_zoom = self.map.min_level zoom = self.map.zoom_factor scale = self.diagram.min_branch_width + (zoom - min_zoom) / (max_zoom - min_zoom) + return scale + + def update_device_sizes(self): + """ + + :return: + """ # rescale lines for dev_tpe in [DeviceType.LineDevice, @@ -889,14 +900,14 @@ def update_device_sizes(self): DeviceType.HVDCLineDevice, DeviceType.FluidPathDevice]: graphics_dict = self.graphics_manager.get_device_type_dict(device_type=dev_tpe) - for key, lne in graphics_dict.items(): - lne.set_width_scale(scale) + for key, elm_graphics in graphics_dict.items(): + elm_graphics.set_width_scale(self.get_branch_width()) # rescale substations data: Dict[str, SubstationGraphicItem] = self.graphics_manager.get_device_type_dict(DeviceType.SubstationDevice) - for se_key, se in data.items(): - se.set_api_object_color() - se.set_size(r=self.diagram.min_bus_width) + for se_key, elm_graphics in data.items(): + elm_graphics.set_api_object_color() + elm_graphics.set_size(r=self.diagram.min_bus_width) def change_size_and_pen_width_all(self, new_radius, pen_width): """ @@ -1070,9 +1081,10 @@ def colour_results(self, weight = int( np.floor(min_branch_width + Sfnorm[i] * (max_branch_width - min_branch_width) * 0.1)) else: - weight = 0.5 + weight = self.get_branch_width() - graphic_object.set_colour(color=color, w=weight, style=style, tool_tip=tooltip) + graphic_object.set_colour(color=color, style=style, tool_tip=tooltip) + graphic_object.set_width_scale(weight) if hasattr(graphic_object, 'set_arrows_with_power'): graphic_object.set_arrows_with_power( @@ -1130,7 +1142,7 @@ def colour_results(self, weight = int( np.floor(min_branch_width + Sfnorm[i] * (max_branch_width - min_branch_width) * 0.1)) else: - weight = 0.5 + weight = self.get_branch_width() tooltip = str(i) + ': ' + graphic_object.api_object.name tooltip += '\n' + loading_label + ': ' + "{:10.4f}".format( @@ -1145,7 +1157,8 @@ def colour_results(self, else: graphic_object.set_arrows_with_hvdc_power(Pf=hvdc_Pf[i], Pt=-hvdc_Pf[i]) - graphic_object.set_colour(color=color, w=weight, style=style, tool_tip=tooltip) + graphic_object.set_colour(color=color, style=style, tool_tip=tooltip) + graphic_object.set_width_scale(weight) def get_image(self, transparent: bool = False) -> QImage: """ @@ -1213,6 +1226,7 @@ def copy(self) -> "GridMapWidget": logger=self.logger) return GridMapWidget( + gui=self.gui, tile_src=self.map.tile_src, start_level=self.diagram.start_level, longitude=self.diagram.longitude, @@ -1233,6 +1247,80 @@ def consolidate_coordinates(self): gelm.api_object.latitude = gelm.lat gelm.api_object.longitude = gelm.lon + def plot_substation(self, i: int, api_object: Substation): + """ + Plot branch results + :param i: bus index + :param api_object: Substation API object + :return: + """ + + fig = plt.figure(figsize=(12, 8)) + ax_1 = fig.add_subplot(211) + ax_1.set_title('Power', fontsize=14) + ax_1.set_ylabel('Injections [MW]', fontsize=11) + + ax_2 = fig.add_subplot(212, sharex=ax_1) + ax_2.set_title('Time', fontsize=14) + ax_2.set_ylabel('Voltage [p.u]', fontsize=11) + + # set time + x = self.circuit.get_time_array() + + if x is not None: + if len(x) > 0: + + # Get all devices grouped by bus + all_data = self.circuit.get_injection_devices_grouped_by_substation() + + # search drivers for voltage data + for driver, results in self.gui.session.drivers_results_iter(): + if results is not None: + if isinstance(results, PowerFlowTimeSeriesResults): + table = results.mdl(result_type=ResultTypes.BusVoltageModule) + table.plot_device(ax=ax_2, device_idx=i) + elif isinstance(results, OptimalPowerFlowTimeSeriesResults): + table = results.mdl(result_type=ResultTypes.BusVoltageModule) + table.plot_device(ax=ax_2, device_idx=i) + + # Injections + # filter injections by bus + bus_devices = all_data.get(api_object, None) + if bus_devices: + + power_data = dict() + for tpe_name, devices in bus_devices.items(): + for device in devices: + if device.device_type == DeviceType.LoadDevice: + power_data[device.name] = -device.P_prof.toarray() + elif device.device_type == DeviceType.GeneratorDevice: + power_data[device.name] = device.P_prof.toarray() + elif device.device_type == DeviceType.ShuntDevice: + power_data[device.name] = -device.G_prof.toarray() + elif device.device_type == DeviceType.StaticGeneratorDevice: + power_data[device.name] = device.P_prof.toarray() + elif device.device_type == DeviceType.ExternalGridDevice: + power_data[device.name] = device.P_prof.toarray() + elif device.device_type == DeviceType.BatteryDevice: + power_data[device.name] = device.P_prof.toarray() + else: + raise Exception("Missing shunt device for plotting") + + df = pd.DataFrame(data=power_data, index=x) + + try: + # yt area plots + df.plot.area(ax=ax_1) + except ValueError: + # use regular plots + df.plot(ax=ax_1) + + plt.legend() + fig.suptitle(api_object.name, fontsize=20) + + # plot the profiles + plt.show() + def generate_map_diagram(substations: List[Substation], voltage_levels: List[VoltageLevel], diff --git a/src/GridCal/Gui/Diagrams/MapWidget/map_widget.py b/src/GridCal/Gui/Diagrams/MapWidget/map_widget.py index eb295467f..a6e6a8115 100644 --- a/src/GridCal/Gui/Diagrams/MapWidget/map_widget.py +++ b/src/GridCal/Gui/Diagrams/MapWidget/map_widget.py @@ -124,7 +124,7 @@ def __init__(self, # Create a QGraphicsProxyWidget for the QLabel self.label_proxy_widget = QGraphicsProxyWidget() self.label_proxy_widget.setWidget(self.attribution_label) - self.label_proxy_widget.setFlag(QGraphicsItem.ItemIgnoresTransformations) + self.label_proxy_widget.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations) self.update_label_position() # Add the proxy widget to the scene @@ -150,6 +150,8 @@ def __init__(self, self.scale(initial_zoom_factor, initial_zoom_factor) + self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform) + def set_notice(self, val: str): """ diff --git a/src/GridCal/Gui/Diagrams/SchematicWidget/Substation/bus_graphics.py b/src/GridCal/Gui/Diagrams/SchematicWidget/Substation/bus_graphics.py index b0c6fe1aa..a5cbd0596 100644 --- a/src/GridCal/Gui/Diagrams/SchematicWidget/Substation/bus_graphics.py +++ b/src/GridCal/Gui/Diagrams/SchematicWidget/Substation/bus_graphics.py @@ -452,87 +452,6 @@ def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent): icon_path=":/Icons/icons/plot.svg", function_ptr=self.plot_profiles) - - # arr = menu.addAction('Arrange') - # arr_icon = QIcon() - # arr_icon.addPixmap(QPixmap(":/Icons/icons/automatic_layout.svg")) - # arr.setIcon(arr_icon) - # arr.triggered.connect(self.arrange_children) - # - # ra5 = menu.addAction('Assign active state to profile') - # ra5_icon = QIcon() - # ra5_icon.addPixmap(QPixmap(":/Icons/icons/assign_to_profile.svg")) - # ra5.setIcon(ra5_icon) - # ra5.triggered.connect(self.assign_status_to_profile) - # - # ra3 = menu.addAction('Delete all the connections') - # del2_icon = QIcon() - # del2_icon.addPixmap(QPixmap(":/Icons/icons/delete_conn.svg")) - # ra3.setIcon(del2_icon) - # ra3.triggered.connect(self.delete_all_connections) - # - # da = menu.addAction('Delete') - # del_icon = QIcon() - # del_icon.addPixmap(QPixmap(":/Icons/icons/delete_db.svg")) - # da.setIcon(del_icon) - # da.triggered.connect(self.remove) - # - # re = menu.addAction('Expand schematic') - # re_icon = QIcon() - # re_icon.addPixmap(QPixmap(":/Icons/icons/grid_icon.svg")) - # re.setIcon(re_icon) - # re.triggered.connect(self.expand_diagram_from_bus) - # - # menu.addSection("Add") - # - # al = menu.addAction('Load') - # al_icon = QIcon() - # al_icon.addPixmap(QPixmap(":/Icons/icons/add_load.svg")) - # al.setIcon(al_icon) - # al.triggered.connect(self.add_load) - # - # ac_i = menu.addAction('Current injection') - # ac_i_icon = QIcon() - # ac_i_icon.addPixmap(QPixmap(":/Icons/icons/add_load.svg")) - # ac_i.setIcon(ac_i_icon) - # ac_i.triggered.connect(self.add_current_injection) - # - # ash = menu.addAction('Shunt') - # ash_icon = QIcon() - # ash_icon.addPixmap(QPixmap(":/Icons/icons/add_shunt.svg")) - # ash.setIcon(ash_icon) - # ash.triggered.connect(self.add_shunt) - # - # acsh = menu.addAction('Controllable shunt') - # acsh_icon = QIcon() - # acsh_icon.addPixmap(QPixmap(":/Icons/icons/add_shunt.svg")) - # acsh.setIcon(acsh_icon) - # acsh.triggered.connect(self.add_controllable_shunt) - # - # acg = menu.addAction('Generator') - # acg_icon = QIcon() - # acg_icon.addPixmap(QPixmap(":/Icons/icons/add_gen.svg")) - # acg.setIcon(acg_icon) - # acg.triggered.connect(self.add_generator) - # - # asg = menu.addAction('Static generator') - # asg_icon = QIcon() - # asg_icon.addPixmap(QPixmap(":/Icons/icons/add_stagen.svg")) - # asg.setIcon(asg_icon) - # asg.triggered.connect(self.add_static_generator) - # - # ab = menu.addAction('Battery') - # ab_icon = QIcon() - # ab_icon.addPixmap(QPixmap(":/Icons/icons/add_batt.svg")) - # ab.setIcon(ab_icon) - # ab.triggered.connect(self.add_battery) - # - # aeg = menu.addAction('External grid') - # aeg_icon = QIcon() - # aeg_icon.addPixmap(QPixmap(":/Icons/icons/add_external_grid.svg")) - # aeg.setIcon(aeg_icon) - # aeg.triggered.connect(self.add_external_grid) - add_menu_entry(menu, text='Arrange', icon_path=":/Icons/icons/automatic_layout.svg", diff --git a/src/GridCal/Gui/Diagrams/SchematicWidget/schematic_widget.py b/src/GridCal/Gui/Diagrams/SchematicWidget/schematic_widget.py index 9b191eddf..4308fb356 100644 --- a/src/GridCal/Gui/Diagrams/SchematicWidget/schematic_widget.py +++ b/src/GridCal/Gui/Diagrams/SchematicWidget/schematic_widget.py @@ -53,7 +53,8 @@ from GridCalEngine.Devices.Fluid import FluidNode, FluidPath from GridCalEngine.Devices.Diagrams.schematic_diagram import SchematicDiagram from GridCalEngine.Devices.Diagrams.graphic_location import GraphicLocation -from GridCalEngine.Simulations import PowerFlowTimeSeriesResults +from GridCalEngine.Simulations.OPF.opf_ts_results import OptimalPowerFlowTimeSeriesResults +from GridCalEngine.Simulations.PowerFlow.power_flow_ts_results import PowerFlowTimeSeriesResults from GridCalEngine.enumerations import DeviceType, ResultTypes from GridCalEngine.basic_structures import Vec, CxVec, IntVec, Logger @@ -3736,7 +3737,10 @@ def plot_bus(self, i: int, api_object: Bus): if results is not None: if isinstance(results, PowerFlowTimeSeriesResults): table = results.mdl(result_type=ResultTypes.BusVoltageModule) - table.plot(ax=ax_2, selected_col_idx=[i]) + table.plot_device(ax=ax_2, device_idx=i) + elif isinstance(results, OptimalPowerFlowTimeSeriesResults): + table = results.mdl(result_type=ResultTypes.BusVoltageModule) + table.plot_device(ax=ax_2, device_idx=i) # Injections # filter injections by bus diff --git a/src/GridCal/Gui/Diagrams/base_diagram_widget.py b/src/GridCal/Gui/Diagrams/base_diagram_widget.py index 1e6f27f5e..acc0079d6 100644 --- a/src/GridCal/Gui/Diagrams/base_diagram_widget.py +++ b/src/GridCal/Gui/Diagrams/base_diagram_widget.py @@ -325,47 +325,47 @@ def plot_branch(self, i: int, api_object: Union[Line, DcLine, Transformer2W, VSC if isinstance(results, PowerFlowTimeSeriesResults): Sf_table = results.mdl(result_type=ResultTypes.BranchActivePowerFrom) - Sf_table.plot(ax=ax_1, selected_col_idx=[i]) + Sf_table.plot_device(ax=ax_1, device_idx=i) loading_table = results.mdl(result_type=ResultTypes.BranchLoading) loading_table.convert_to_cdf() - loading_table.plot(ax=ax_2, selected_col_idx=[i]) + loading_table.plot_device(ax=ax_2, device_idx=i) any_plot = True elif isinstance(results, LinearAnalysisTimeSeriesResults): Sf_table = results.mdl(result_type=ResultTypes.BranchActivePowerFrom) - Sf_table.plot(ax=ax_1, selected_col_idx=[i]) + Sf_table.plot_device(ax=ax_1, device_idx=i) loading_table = results.mdl(result_type=ResultTypes.BranchLoading) loading_table.convert_to_cdf() - loading_table.plot(ax=ax_2, selected_col_idx=[i]) + loading_table.plot_device(ax=ax_2, device_idx=i) any_plot = True elif isinstance(results, ContingencyAnalysisTimeSeriesResults): Sf_table = results.mdl(result_type=ResultTypes.MaxContingencyFlows) - Sf_table.plot(ax=ax_1, selected_col_idx=[i]) + Sf_table.plot_device(ax=ax_1, device_idx=i) loading_table = results.mdl(result_type=ResultTypes.MaxContingencyLoading) loading_table.convert_to_cdf() - loading_table.plot(ax=ax_2, selected_col_idx=[i]) + loading_table.plot_device(ax=ax_2, device_idx=i) any_plot = True elif isinstance(results, OptimalPowerFlowTimeSeriesResults): Sf_table = results.mdl(result_type=ResultTypes.BranchActivePowerFrom) - Sf_table.plot(ax=ax_1, selected_col_idx=[i]) + Sf_table.plot_device(ax=ax_1, device_idx=i) loading_table = results.mdl(result_type=ResultTypes.BranchLoading) loading_table.convert_to_cdf() - loading_table.plot(ax=ax_2, selected_col_idx=[i]) + loading_table.plot_device(ax=ax_2, device_idx=i) any_plot = True elif isinstance(results, StochasticPowerFlowResults): loading_table = results.mdl(result_type=ResultTypes.BranchLoadingAverage) loading_table.convert_to_cdf() - loading_table.plot(ax=ax_2, selected_col_idx=[i]) + loading_table.plot_device(ax=ax_2, device_idx=i) any_plot = True if any_plot: diff --git a/src/GridCal/Gui/Main/MainWindow.py b/src/GridCal/Gui/Main/MainWindow.py index f45db8489..09e2e1fbf 100644 --- a/src/GridCal/Gui/Main/MainWindow.py +++ b/src/GridCal/Gui/Main/MainWindow.py @@ -965,8 +965,9 @@ def setupUi(self, mainWindow): self.max_node_size_spinBox = QDoubleSpinBox(self.frame_58) self.max_node_size_spinBox.setObjectName(u"max_node_size_spinBox") self.max_node_size_spinBox.setFont(font2) - self.max_node_size_spinBox.setDecimals(1) - self.max_node_size_spinBox.setMinimum(0.100000000000000) + self.max_node_size_spinBox.setDecimals(3) + self.max_node_size_spinBox.setMinimum(0.010000000000000) + self.max_node_size_spinBox.setMaximum(9999.000000000000000) self.max_node_size_spinBox.setSingleStep(0.100000000000000) self.max_node_size_spinBox.setValue(40.000000000000000) @@ -975,7 +976,9 @@ def setupUi(self, mainWindow): self.max_branch_size_spinBox = QDoubleSpinBox(self.frame_58) self.max_branch_size_spinBox.setObjectName(u"max_branch_size_spinBox") self.max_branch_size_spinBox.setFont(font2) - self.max_branch_size_spinBox.setDecimals(1) + self.max_branch_size_spinBox.setDecimals(3) + self.max_branch_size_spinBox.setMinimum(0.010000000000000) + self.max_branch_size_spinBox.setMaximum(9999.000000000000000) self.max_branch_size_spinBox.setSingleStep(0.100000000000000) self.max_branch_size_spinBox.setValue(20.000000000000000) @@ -990,17 +993,20 @@ def setupUi(self, mainWindow): self.min_node_size_spinBox = QDoubleSpinBox(self.frame_58) self.min_node_size_spinBox.setObjectName(u"min_node_size_spinBox") self.min_node_size_spinBox.setFont(font2) - self.min_node_size_spinBox.setDecimals(1) - self.min_node_size_spinBox.setMinimum(0.100000000000000) + self.min_node_size_spinBox.setDecimals(3) + self.min_node_size_spinBox.setMinimum(0.010000000000000) + self.min_node_size_spinBox.setMaximum(9999.000000000000000) self.min_node_size_spinBox.setSingleStep(0.100000000000000) - self.min_node_size_spinBox.setValue(20.000000000000000) + self.min_node_size_spinBox.setValue(5.000000000000000) self.gridLayout.addWidget(self.min_node_size_spinBox, 10, 1, 1, 1) self.min_branch_size_spinBox = QDoubleSpinBox(self.frame_58) self.min_branch_size_spinBox.setObjectName(u"min_branch_size_spinBox") self.min_branch_size_spinBox.setFont(font2) - self.min_branch_size_spinBox.setDecimals(1) + self.min_branch_size_spinBox.setDecimals(3) + self.min_branch_size_spinBox.setMinimum(0.010000000000000) + self.min_branch_size_spinBox.setMaximum(9999.000000000000000) self.min_branch_size_spinBox.setSingleStep(0.100000000000000) self.min_branch_size_spinBox.setValue(0.500000000000000) diff --git a/src/GridCal/Gui/Main/MainWindow.ui b/src/GridCal/Gui/Main/MainWindow.ui index a3eb8b3a7..00c895b20 100644 --- a/src/GridCal/Gui/Main/MainWindow.ui +++ b/src/GridCal/Gui/Main/MainWindow.ui @@ -1076,10 +1076,13 @@ QProgressBar::chunk{ px - 1 + 3 - 0.100000000000000 + 0.010000000000000 + + + 9999.000000000000000 0.100000000000000 @@ -1100,7 +1103,13 @@ QProgressBar::chunk{ px - 1 + 3 + + + 0.010000000000000 + + + 9999.000000000000000 0.100000000000000 @@ -1133,16 +1142,19 @@ QProgressBar::chunk{ px - 1 + 3 - 0.100000000000000 + 0.010000000000000 + + + 9999.000000000000000 0.100000000000000 - 20.000000000000000 + 5.000000000000000 @@ -1157,7 +1169,13 @@ QProgressBar::chunk{ px - 1 + 3 + + + 0.010000000000000 + + + 9999.000000000000000 0.100000000000000 diff --git a/src/GridCal/Gui/Main/SubClasses/Model/time_events.py b/src/GridCal/Gui/Main/SubClasses/Model/time_events.py index 7b2c391bd..6090e85a2 100644 --- a/src/GridCal/Gui/Main/SubClasses/Model/time_events.py +++ b/src/GridCal/Gui/Main/SubClasses/Model/time_events.py @@ -21,9 +21,10 @@ from matplotlib import pyplot as plt import GridCal.Gui.gui_functions as gf +from GridCalEngine.basic_structures import Logger from GridCalEngine.enumerations import DeviceType from GridCalEngine.Devices.types import ALL_DEV_TYPES -from GridCal.Gui.general_dialogues import NewProfilesStructureDialogue, TimeReIndexDialogue +from GridCal.Gui.general_dialogues import NewProfilesStructureDialogue, TimeReIndexDialogue, LogsDialogue from GridCal.Gui.messages import yes_no_question, warning_msg, info_msg from GridCal.Gui.Main.SubClasses.Model.objects import ObjectsTableMain from GridCal.Gui.ProfilesInput.models_dialogue import ModelsInputGUI @@ -328,7 +329,7 @@ def set_profile_as_linear_combination(self): Edit profiles with a linear combination Returns: Nothing """ - + logger: Logger = Logger() # value = self.ui.profile_factor_doubleSpinBox.value() dev_type_text = self.get_db_object_selected_type() @@ -356,13 +357,14 @@ def set_profile_as_linear_combination(self): # Assign profiles if len(objects) > 0: - attr_from = objects[0].properties_with_profile[magnitude_from] - attr_to = objects[0].properties_with_profile[magnitude_to] for i, elm in enumerate(objects): - profile_from = elm.get_profile(magnitude=attr_from) - profile_to = elm.get_profile(magnitude=attr_to) - profile_to.set(profile_from.toarray()) + profile_from = elm.get_profile(magnitude=magnitude_from) + profile_to = elm.get_profile(magnitude=magnitude_to) + if profile_from is not None and profile_to is not None: + profile_to.set(profile_from.toarray()) + else: + print(f"P or Q profile None in {elm.name}") self.display_profiles() @@ -374,6 +376,10 @@ def set_profile_as_linear_combination(self): # no buses or no actual change pass + if logger.has_logs(): + dlg = LogsDialogue("Set profile", logger=logger) + dlg.exec_() + def re_index_time(self): """ Re-index time diff --git a/src/GridCal/__version__.py b/src/GridCal/__version__.py index 004781418..3990df210 100644 --- a/src/GridCal/__version__.py +++ b/src/GridCal/__version__.py @@ -16,7 +16,7 @@ _current_year_ = datetime.datetime.now().year # do not forget to keep a three-number version!!! -__GridCal_VERSION__ = "5.1.43" +__GridCal_VERSION__ = "5.1.45" url = 'https://github.com/SanPen/GridCal' diff --git a/src/GridCalEngine/Devices/assets.py b/src/GridCalEngine/Devices/assets.py index a57d39626..9f79599fb 100644 --- a/src/GridCalEngine/Devices/assets.py +++ b/src/GridCalEngine/Devices/assets.py @@ -504,11 +504,6 @@ def ensure_profiles_exist(self) -> None: if self.time_profile is None: raise Exception('Cannot ensure profiles existence without a time index. Try format_profiles instead') - # for key, tpe in self.device_type_name_dict.items(): - # elements = self.get_elements_by_type(device_type=tpe) - # for elm in elements: - # elm.ensure_profiles_exist(self.time_profile) - for elm in self.items(): elm.ensure_profiles_exist(self.time_profile) @@ -4321,7 +4316,6 @@ def injection_items(self) -> Generator[INJECTION_DEVICE_TYPES, None, None]: for elm in lst: yield elm - # ------------------------------------------------------------------------------------------------------------------ # Load-like devices # ------------------------------------------------------------------------------------------------------------------ @@ -4508,28 +4502,28 @@ def get_elements_by_type(self, device_type: DeviceType) -> Union[pd.DatetimeInde """ if device_type == DeviceType.LoadDevice: - return self.get_loads() + return self._loads elif device_type == DeviceType.StaticGeneratorDevice: - return self.get_static_generators() + return self._static_generators elif device_type == DeviceType.GeneratorDevice: - return self.get_generators() + return self._generators elif device_type == DeviceType.BatteryDevice: - return self.get_batteries() + return self._batteries elif device_type == DeviceType.ShuntDevice: - return self.get_shunts() + return self._shunts elif device_type == DeviceType.ExternalGridDevice: - return self.get_external_grids() + return self._external_grids elif device_type == DeviceType.CurrentInjectionDevice: - return self.get_current_injections() + return self._current_injections elif device_type == DeviceType.ControllableShuntDevice: - return self.get_controllable_shunts() + return self._controllable_shunts elif device_type == DeviceType.LineDevice: return self._lines @@ -4641,15 +4635,6 @@ def get_elements_by_type(self, device_type: DeviceType) -> Union[pd.DatetimeInde elif device_type == DeviceType.EmissionGasDevice: return self._emission_gases - # elif device_type == DeviceType.GeneratorTechnologyAssociation: - # return self._generators_technologies - # - # elif device_type == DeviceType.GeneratorFuelAssociation: - # return self._generators_fuels - # - # elif device_type == DeviceType.GeneratorEmissionAssociation: - # return self._generators_emissions - elif device_type == DeviceType.ConnectivityNodeDevice: return self._connectivity_nodes @@ -5296,7 +5281,7 @@ def add_or_replace_object(self, api_obj: ALL_DEV_TYPES, logger: Logger) -> bool: return found - def get_all_elements_dict(self, logger = Logger()) -> Tuple[Dict[str, ALL_DEV_TYPES], bool]: + def get_all_elements_dict(self, logger=Logger()) -> Tuple[Dict[str, ALL_DEV_TYPES], bool]: """ Get a dictionary of all elements :param: logger: Logger diff --git a/src/GridCalEngine/Devices/multi_circuit.py b/src/GridCalEngine/Devices/multi_circuit.py index 13023f964..25e1d9204 100644 --- a/src/GridCalEngine/Devices/multi_circuit.py +++ b/src/GridCalEngine/Devices/multi_circuit.py @@ -1309,6 +1309,32 @@ def change_base(self, Sbase_new: float): # assign the new base self.Sbase = Sbase_new + def get_injection_devices_grouped_by_substation(self) -> Dict[dev.Substation, Dict[DeviceType, List[INJECTION_DEVICE_TYPES]]]: + """ + Get the injection devices grouped by bus and by device type + :return: Dict[bus, Dict[DeviceType, List[Injection devs]] + """ + groups: Dict[dev.Substation, Dict[DeviceType, List[INJECTION_DEVICE_TYPES]]] = dict() + + for lst in self.get_injection_devices_lists(): + + for elm in lst: + + if elm.bus.substation is not None: + + devices_by_type = groups.get(elm.bus.substation, None) + + if devices_by_type is None: + groups[elm.bus.substation] = {elm.device_type: [elm]} + else: + lst = devices_by_type.get(elm.device_type, None) + if lst is None: + devices_by_type[elm.device_type] = [elm] + else: + devices_by_type[elm.device_type].append(elm) + + return groups + def get_injection_devices_grouped_by_bus(self) -> Dict[dev.Bus, Dict[DeviceType, List[INJECTION_DEVICE_TYPES]]]: """ Get the injection devices grouped by bus and by device type diff --git a/src/GridCalEngine/Devices/profile.py b/src/GridCalEngine/Devices/profile.py index d9fd30a3c..fa003b577 100644 --- a/src/GridCalEngine/Devices/profile.py +++ b/src/GridCalEngine/Devices/profile.py @@ -19,6 +19,8 @@ from collections import Counter import numpy as np import numba as nb +from numpy import dtype + from GridCalEngine.basic_structures import Numeric, NumericVec, IntVec from GridCalEngine.enumerations import DeviceType from GridCalEngine.Utils.Sparse.sparse_array import SparseArray, PROFILE_TYPES, check_type @@ -336,7 +338,16 @@ def __getitem__(self, key: int): if self._is_sparse: return self._sparse_array[key] else: - return self._dense_array[key] + + if self._dense_array is None: + # WTF, initialize sparse + self._is_sparse = True + self._sparse_array = SparseArray(data_type=self.dtype) + self._sparse_array.default_value = self.default_value + print("Initializing sparse when querying, this signals a mis initilaization") + return self.default_value + else: + return self._dense_array[key] def __setitem__(self, key: int, value): """ @@ -392,7 +403,12 @@ def resize(self, n: int): if self._is_sparse: self._sparse_array.resize(n=n) else: - self._dense_array.resize(n) + try: + self._dense_array.resize(n) + except ValueError: + new_arr = np.zeros(n, dtype=self._dense_array.dtype) + new_arr[:len(self._dense_array)] = self._dense_array + self._dense_array = new_arr # this is to avoid ValueError when resizing a numpy array of Objects else: self._initialized = True self.create_sparse(size=n, default_value=self.default_value) diff --git a/src/GridCalEngine/IO/gridcal/pack_unpack.py b/src/GridCalEngine/IO/gridcal/pack_unpack.py index 72b310f82..311ed1856 100644 --- a/src/GridCalEngine/IO/gridcal/pack_unpack.py +++ b/src/GridCalEngine/IO/gridcal/pack_unpack.py @@ -267,8 +267,7 @@ def profile_todict(profile: Profile) -> Dict[str, str]: return { 'is_sparse': True, 'size': s, - 'default': profile.default_value - if profile.sparse_array is None else profile.sparse_array.default_value, + 'default': profile.default_value if profile.sparse_array is None else profile.sparse_array.default_value, 'sparse_data': { 'map': dict() } @@ -288,8 +287,9 @@ def profile_todict_idtag(profile: Profile) -> Dict[str, str]: 'size': profile.size(), 'default': default, 'sparse_data': { - 'map': {key: val.idtag for key, val in profile.sparse_array.get_map().items()} - if profile.sparse_array else dict() + 'map': {key: val.idtag if hasattr(val, 'idtag') else None + for key, val in profile.sparse_array.get_map().items()} + if profile.sparse_array is not None else dict() } } else: @@ -297,7 +297,8 @@ def profile_todict_idtag(profile: Profile) -> Dict[str, str]: 'is_sparse': profile.is_sparse, 'size': profile.size(), 'default': default, - 'dense_data': [e.idtag for e in profile.dense_array] if profile.dense_array else list(), + 'dense_data': [e.idtag if hasattr(e, 'idtag') else None for e in profile.dense_array] + if profile.dense_array is not None else list(), } @@ -434,6 +435,10 @@ def gather_model_as_jsons(circuit: MultiCircuit) -> Dict[str, Dict[str, str]]: :param circuit: :return: """ + + if circuit.has_time_series: + circuit.ensure_profiles_exist() + data: Dict[str, Union[Dict[str, str], List[Dict[str, str]]]] = dict() # declare objects to iterate name: [sample object, list of objects, headers] @@ -1469,4 +1474,7 @@ def parse_gridcal_data(data: Dict[str, Union[str, float, pd.DataFrame, Dict[str, if text_func is not None: text_func("Done!") + if circuit.has_time_series: + circuit.ensure_profiles_exist() + return circuit diff --git a/src/GridCalEngine/Simulations/results_table.py b/src/GridCalEngine/Simulations/results_table.py index 768c9afdf..5cb65540f 100644 --- a/src/GridCalEngine/Simulations/results_table.py +++ b/src/GridCalEngine/Simulations/results_table.py @@ -374,3 +374,36 @@ def plot(self, ax=None, selected_col_idx=None, selected_rows=None, stacked=False df.plot(ax=ax, legend=plot_legend, stacked=stacked) except TypeError: print('No numeric data to plot...') + + def plot_device(self, ax=None, device_idx: int = 0, stacked=False): + """ + Plot the data model + :param ax: Matplotlib axis + :param device_idx: list of selected column indices + :param stacked: Stack plot? + """ + index, columns, data = self.get_data() + + columns = [columns[device_idx]] + data = data[:, device_idx] + + if ax is None: + fig = plt.figure(figsize=(12, 6)) + ax = fig.add_subplot(111) + + if 'voltage' in self.title.lower(): + data[data == 0] = 'nan' # to avoid plotting the zeros + + if len(columns) > 15: + plot_legend = False + else: + plot_legend = True + + df = pd.DataFrame(data=data, index=index, columns=columns) + ax.set_title(self.title, fontsize=14) + ax.set_ylabel(self.y_label, fontsize=11) + ax.set_xlabel(self.x_label, fontsize=11) + try: + df.plot(ax=ax, legend=plot_legend, stacked=stacked) + except TypeError: + print('No numeric data to plot...') diff --git a/src/GridCalEngine/__version__.py b/src/GridCalEngine/__version__.py index dcc6a2e9f..7d2d538bf 100644 --- a/src/GridCalEngine/__version__.py +++ b/src/GridCalEngine/__version__.py @@ -16,7 +16,7 @@ _current_year_ = datetime.datetime.now().year # do not forget to keep a three-number version!!! -__GridCalEngine_VERSION__ = "5.1.43" +__GridCalEngine_VERSION__ = "5.1.45" url = 'https://github.com/SanPen/GridCal' diff --git a/src/GridCalEngine/setup.py b/src/GridCalEngine/setup.py index bd02d22e1..e03aba8c8 100644 --- a/src/GridCalEngine/setup.py +++ b/src/GridCalEngine/setup.py @@ -56,7 +56,7 @@ "scipy>=1.0.0", "networkx>=2.1", "pandas>=1.0", - "ortools>=9.8.0,<=9.9.3963", + "ortools>=9.10.0", "xlwt>=1.3.0", "xlrd>=1.1.0", "matplotlib>=2.1.1", diff --git a/src/GridCalServer/__version__.py b/src/GridCalServer/__version__.py index 7e67a6747..3afed76e2 100644 --- a/src/GridCalServer/__version__.py +++ b/src/GridCalServer/__version__.py @@ -16,7 +16,7 @@ _current_year_ = datetime.datetime.now().year # do not forget to keep a three-number version!!! -__GridCalServer_VERSION__ = "5.1.43" +__GridCalServer_VERSION__ = "5.1.45" url = 'https://github.com/SanPen/GridCal'