From f4489975b1926e5f7942c226df4e7d3e624255b7 Mon Sep 17 00:00:00 2001 From: Toru Seo <34780089+toruseo@users.noreply.github.com> Date: Sat, 11 May 2024 15:38:38 +0900 Subject: [PATCH 1/2] Add vehicle tracker in GUI --- uxsim/ResultGUIViewer/ResultGUIViewer.py | 78 +++++++++++++++++++----- uxsim/analyzer.py | 27 +++++++- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/uxsim/ResultGUIViewer/ResultGUIViewer.py b/uxsim/ResultGUIViewer/ResultGUIViewer.py index 60640d4..95be6fe 100644 --- a/uxsim/ResultGUIViewer/ResultGUIViewer.py +++ b/uxsim/ResultGUIViewer/ResultGUIViewer.py @@ -15,9 +15,9 @@ import sys import numpy as np from matplotlib import colormaps -from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu, QSlider, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QPushButton +from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu, QSlider, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QPushButton, QInputDialog, QMessageBox, QTableView from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath -from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer +from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer, QAbstractTableModel class EdgeItem(QGraphicsItem): def __init__(self, name, start_node, end_node, density_list, Link): @@ -44,7 +44,7 @@ def shape(self): length = (dx**2 + dy**2)**0.5 offset = length/10 if dx == 0: - offset = QPointF(0, offset if dy > 0 else -offset) + offset = QPointF(-offset*self.curve_direction if dy > 0 else offset*self.curve_direction, 0) else: normal = QPointF(-dy, dx) normal /= (normal.x()**2 + normal.y()**2)**0.5 @@ -188,9 +188,12 @@ def __init__(self, nodes, edges, vehicle_list): edge = EdgeItem(name, start_node, end_node, density_list, Node) self.scene().addItem(edge) self.edges.append(edge) + + self.set_vehice_items() + def set_vehice_items(self): self.vehicle_items = [] - if self.show_vehicles and self.vehicle_list != None: + if self.vehicle_list != None: for t, edge_name, x in self.vehicle_list: edge = self.find_edge(edge_name) if edge: @@ -253,15 +256,16 @@ def set_show_vehicles(self, show_vehicles): self.viewport().update() class MainWindow(QMainWindow): - def __init__(self, W, nodes, edges, vehicle_list, tmax): + def __init__(self, W, nodes, edges, vehicle_list, tmax, dt): super().__init__() self.setWindowTitle("UXsim result viewer") self.W = W self.tmax = tmax + self.dt = dt self.playing = False self.curve_direction = 1 self.show_names = True - self.show_vehicles = True + self.show_vehicles = False central_widget = QWidget() layout = QVBoxLayout() @@ -304,6 +308,7 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax): # menu_file = menu_bar.addMenu("File") # acrion_save_world = menu_file.addAction("Save World") # acrion_save_world.triggered.connect(lambda: self.save_world()) + menu_settings = menu_bar.addMenu("Settings") option_curve_direction = menu_settings.addMenu("Link Curve Direction") @@ -318,6 +323,14 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax): show_names_action.setChecked(True) show_names_action.triggered.connect(self.toggle_show_names) + menu_Vehicle = menu_bar.addMenu("Vehicle Analysis") + # show_vehicles_action = menu_Vehicle.addAction("Show Vehicle") + # show_vehicles_action.setCheckable(True) + # show_vehicles_action.setChecked(False) + # show_vehicles_action.triggered.connect(self.toggle_show_vehicles) + action_show_vehicle = menu_Vehicle.addAction("Highlight Vehicle by ID") + action_show_vehicle.triggered.connect(self.show_vehicle_by_id) + menu_Animation = menu_bar.addMenu("Export Results") action_csv = menu_Animation.addAction("Export Results to CSV files") action_csv.triggered.connect(lambda: self.W.analyzer.output_data()) @@ -328,12 +341,6 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax): action_network_anim_fancy = menu_Animation.addAction("Export Network Animation (vehicle-level)") action_network_anim_fancy.triggered.connect(lambda: self.W.analyzer.network_fancy()) - # menu_Vehicle = menu_bar.addMenu("Vehicle Analysis") - # show_vehicles_action = menu_Vehicle.addAction("Show Vehicles") - # show_vehicles_action.setCheckable(True) - # show_vehicles_action.setChecked(True) - # show_vehicles_action.triggered.connect(self.toggle_show_vehicles) - self.update_graph() def save_world(self): @@ -377,6 +384,45 @@ def toggle_show_names(self): def toggle_show_vehicles(self): self.show_vehicles = not self.show_vehicles self.graph_widget.set_show_vehicles(self.show_vehicles) + + def show_vehicle_by_id(self): + vehicle_id, ok = QInputDialog.getText(self, "Highlight Vehicle", "Enter Vehicle ID
Note that fast vehicles will be plotted as multiple dots in the animation.") + if ok and vehicle_id: + self.vehicle_id = vehicle_id + if vehicle_id not in self.W.VEHICLES: + QMessageBox.warning(self, "Vehicle ID not found", "The specified Vehicle ID was not found.") + return + veh = self.W.VEHICLES[vehicle_id] + self.graph_widget.vehicle_list = [(int(veh.log_t[i]/self.dt), veh.log_link[i].name, veh.log_x[i]/veh.log_link[i].length) for i in range(len(veh.log_t)) if veh.log_link[i] != -1] + print(veh, self.graph_widget.vehicle_list) + + self.graph_widget.set_vehice_items() + + self.graph_widget.set_show_vehicles(True) + +class PandasModel(QAbstractTableModel): + def __init__(self, data): + super(PandasModel, self).__init__() + self._data = data + + def rowCount(self, parent=None): + return self._data.shape[0] + + def columnCount(self, parent=None): + return self._data.shape[1] + + def data(self, index, role=Qt.DisplayRole): + if index.isValid() and role == Qt.DisplayRole: + return str(self._data.iloc[index.row(), index.column()]) + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole: + if orientation == Qt.Horizontal: + return str(self._data.columns[section]) + elif orientation == Qt.Vertical: + return str(self._data.index[section]) + return None def launch_World_viewer(W, return_app_window=False): """ @@ -424,10 +470,14 @@ def launch_World_viewer(W, return_app_window=False): node[2] = (maxy - node[2]) / (maxy - miny) * (xysize - xybuffer*2) + xybuffer edges = [[l.name, l.start_node.name, l.end_node.name, l.k_mat, l] for l in W.LINKS] - vehicle_list = None + dt = W.LINKS[0].edie_dt + + veh = list(W.VEHICLES.values())[0] + vehicle_list = [(int(veh.log_t[i]/dt), veh.log_link[i].name, veh.log_x[i]/veh.log_link[i].length) for i in range(len(veh.log_t)) if veh.log_link[i] != -1] + print(vehicle_list) app = QApplication(sys.argv) - window = MainWindow(W, nodes, edges, vehicle_list, tmax) + window = MainWindow(W, nodes, edges, None, tmax, dt) window.show() if return_app_window: return app, window diff --git a/uxsim/analyzer.py b/uxsim/analyzer.py index b27ea18..0a4093a 100644 --- a/uxsim/analyzer.py +++ b/uxsim/analyzer.py @@ -414,7 +414,7 @@ def time_space_diagram_traj_links(s, linkslist, figsize=(12,4), plot_signal=True figsize : tuple of int, optional The size of the figure to be plotted, default is (12,4). plot_signal : bool, optional - Plot the signal red light. + Plot the signal red light. """ if s.W.vehicle_logging_timestep_interval != 1: warnings.warn("vehicle_logging_timestep_interval is not 1. The plot is not exactly accurate.", LoggingWarning) @@ -1191,6 +1191,31 @@ def log_vehicles_to_pandas(s): """ return s.vehicles_to_pandas() + def vehicle_trip_top_pandas(s): + """ + Converts the vehicle trip top to a pandas DataFrame. + + Returns + ------- + pd.DataFrame + A DataFrame containing the top of the vehicle trip logs, with the columns: + - 'name': the name of the vehicle (platoon). + - 'orig': the origin node of the vehicle's trip. + - 'dest': the destination node of the vehicle's trip. + - 'trip_time': the total trip time of the vehicle. + - 'trip_delay': the total delay of the vehicle. + """ + out = [["name", "orig", "dest", "departure_time", "final_state", "travel_time", "average_speed"]] + for veh in s.W.VEHICLES.values(): + veh_dest_name = veh.dest.name if veh.dest != None else None + veh_state = veh.log_state[-1] + veh_ave_speed = np.average([v for v in veh.log_v if v != -1]) + + out.append([veh.name, veh.orig.name, veh_dest_name, veh.departure_time*s.W.DELTAT, veh_state, veh.travel_time, veh_ave_speed]) + + s.df_vehicle_trip = pd.DataFrame(out[1:], columns=out[0]) + return s.df_vehicle_trip + def basic_to_pandas(s): """ Converts the basic stats to a pandas DataFrame. From d8785323b8314dd34073c6c5a9bed4037222a487 Mon Sep 17 00:00:00 2001 From: Toru Seo <34780089+toruseo@users.noreply.github.com> Date: Sat, 11 May 2024 16:30:17 +0900 Subject: [PATCH 2/2] Add dataframe viewer in GUI --- uxsim/ResultGUIViewer/ResultGUIViewer.py | 57 +++++++++++++++++++----- uxsim/analyzer.py | 8 ++-- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/uxsim/ResultGUIViewer/ResultGUIViewer.py b/uxsim/ResultGUIViewer/ResultGUIViewer.py index 95be6fe..17d1811 100644 --- a/uxsim/ResultGUIViewer/ResultGUIViewer.py +++ b/uxsim/ResultGUIViewer/ResultGUIViewer.py @@ -15,10 +15,11 @@ import sys import numpy as np from matplotlib import colormaps -from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu, QSlider, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QPushButton, QInputDialog, QMessageBox, QTableView +from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu, QSlider, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QPushButton, QInputDialog, QMessageBox, QTableView, QDialog, QFileDialog from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer, QAbstractTableModel + class EdgeItem(QGraphicsItem): def __init__(self, name, start_node, end_node, density_list, Link): super().__init__() @@ -68,7 +69,6 @@ def paint(self, painter, option, widget): lw = max([density*self.Link.delta*self.Link.lanes])*(maxlw-minlw)+minlw c = colormaps["viridis"](speed/self.Link.u) - #color = QColor(int(density/self.Link.jam_density * 255), int(density/self.Link.jam_density * 255), 0, 255) color = QColor(int(c[0]*255), int(c[1]*255), int(c[2]*255), 255) pen = QPen(color, lw) painter.setPen(pen) @@ -127,6 +127,7 @@ def set_curve_direction(self, direction): def set_show_name(self, show_name): self.show_name = show_name + class NodeItem(QGraphicsItem): def __init__(self, name, x, y, Node): super().__init__() @@ -149,6 +150,7 @@ def paint(self, painter, option, widget): def set_show_name(self, show_name): self.show_name = show_name + class VehicleItem(QGraphicsItem): def __init__(self, x, y): super().__init__() @@ -162,6 +164,7 @@ def paint(self, painter, option, widget): painter.setBrush(Qt.red) painter.drawEllipse(-5, -5, 10, 10) + class GraphWidget(QGraphicsView): def __init__(self, nodes, edges, vehicle_list): super().__init__() @@ -309,6 +312,17 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax, dt): # acrion_save_world = menu_file.addAction("Save World") # acrion_save_world.triggered.connect(lambda: self.save_world()) + menu_data = menu_bar.addMenu("Data") + action_basic_stats = menu_data.addAction("Basic Statistics") + action_basic_stats.triggered.connect(lambda: self.show_dataframe("Basic", self.W.analyzer.basic_to_pandas())) + action_basic_stats = menu_data.addAction("Link Statistics") + action_basic_stats.triggered.connect(lambda: self.show_dataframe("Link", self.W.analyzer.link_to_pandas())) + action_basic_stats = menu_data.addAction("OD Demand Statistics") + action_basic_stats.triggered.connect(lambda: self.show_dataframe("OD Demand", self.W.analyzer.od_to_pandas())) + action_basic_stats = menu_data.addAction("Vehicle Trip Statistics") + action_basic_stats.triggered.connect(lambda: self.show_dataframe("Vehicle Trip", self.W.analyzer.vehicle_trip_to_pandas())) + action_basic_stats = menu_data.addAction("Vehicle Detailed Statistics") + action_basic_stats.triggered.connect(lambda: self.show_dataframe("Vehicle", self.W.analyzer.vehicles_to_pandas())) menu_settings = menu_bar.addMenu("Settings") option_curve_direction = menu_settings.addMenu("Link Curve Direction") @@ -343,10 +357,22 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax, dt): self.update_graph() - def save_world(self): - import pickle - with open("World.pkl", mode="wb") as f: - pickle.dump(self.W, f) + def show_dataframe(self, title, df): + viewer = DataFrameViewer(df, title, self) + viewer.show() + + def save_world(self, default_filename='untitled.pkl_dill'): + #TODO: do something about "maximum recursion depth exceeded in comparison" error + import dill as pickle + filename, _ = QFileDialog.getSaveFileName(None, 'Save the world', default_filename, 'Pickle (by Dill package) Files (*.pkl_dill);;All Files (*)') + + if filename: + try: + with open(filename, 'wb') as file: + pickle.dump(self.W, file) + print(f'World saved successfully: {filename}') + except Exception as e: + print(f'Error saving object: {str(e)}') def update_graph(self): t = self.t_slider.value() @@ -400,6 +426,7 @@ def show_vehicle_by_id(self): self.graph_widget.set_show_vehicles(True) + class PandasModel(QAbstractTableModel): def __init__(self, data): super(PandasModel, self).__init__() @@ -424,6 +451,20 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): return str(self._data.index[section]) return None + +class DataFrameViewer(QDialog): + def __init__(self, data, title, parent=None): + super(DataFrameViewer, self).__init__(parent) + self.setWindowTitle(title) + self.setLayout(QVBoxLayout()) + self.model = PandasModel(data) + self.view = QTableView() + self.view.setModel(self.model) + self.layout().addWidget(self.view) + + self.resize(1200, 600) + + def launch_World_viewer(W, return_app_window=False): """ Launch the interactive viewer for the simulation result of the given World object. @@ -472,10 +513,6 @@ def launch_World_viewer(W, return_app_window=False): edges = [[l.name, l.start_node.name, l.end_node.name, l.k_mat, l] for l in W.LINKS] dt = W.LINKS[0].edie_dt - veh = list(W.VEHICLES.values())[0] - vehicle_list = [(int(veh.log_t[i]/dt), veh.log_link[i].name, veh.log_x[i]/veh.log_link[i].length) for i in range(len(veh.log_t)) if veh.log_link[i] != -1] - print(vehicle_list) - app = QApplication(sys.argv) window = MainWindow(W, nodes, edges, None, tmax, dt) window.show() diff --git a/uxsim/analyzer.py b/uxsim/analyzer.py index 0a4093a..35e711f 100644 --- a/uxsim/analyzer.py +++ b/uxsim/analyzer.py @@ -1191,7 +1191,7 @@ def log_vehicles_to_pandas(s): """ return s.vehicles_to_pandas() - def vehicle_trip_top_pandas(s): + def vehicle_trip_to_pandas(s): """ Converts the vehicle trip top to a pandas DataFrame. @@ -1202,8 +1202,10 @@ def vehicle_trip_top_pandas(s): - 'name': the name of the vehicle (platoon). - 'orig': the origin node of the vehicle's trip. - 'dest': the destination node of the vehicle's trip. - - 'trip_time': the total trip time of the vehicle. - - 'trip_delay': the total delay of the vehicle. + - 'departure_time': the departure time of the vehicle. + - 'final_state': the final state of the vehicle. + - 'travel_time': the travel time of the vehicle. + - 'average_speed': the average speed of the vehicle. """ out = [["name", "orig", "dest", "departure_time", "final_state", "travel_time", "average_speed"]] for veh in s.W.VEHICLES.values():