diff --git a/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py b/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py index ac173e5f1..78728dad0 100644 --- a/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py +++ b/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py @@ -94,7 +94,7 @@ def update_position(self): if not self.reported_breach: self.reported_breach = True self.append_fail_to_log(f"{self.target_drone};First breach: deviated more than " - f"{self.deviation_percentage}meter from the planned route") + f"{self.deviation_percentage} meter from the planned route") self.breach_flag = True self.est_position_array.append([x, y, z]) self.obj_position_array.append([ox, oy, oz]) @@ -103,12 +103,12 @@ def update_position(self): # print(self.position_array) def check_breach(self, x, y, z): - #print(f"Checking breach, Current position: {round(x, 2)}, {round(y, 2)}, {round(z, 2)}, " + # print(f"Checking breach, Current position: {round(x, 2)}, {round(y, 2)}, {round(z, 2)}, " # f"center: {self.mission.center.x_val}, {self.mission.center.y_val}, {self.mission.altitude}") return GeoUtil.is_point_close_to_circle([self.mission.center.x_val, self.mission.center.y_val, self.mission.altitude], - self.mission.radius, - [x, y, -z], - self.deviation_percentage) + self.mission.radius, + [x, y, -z], + self.deviation_percentage) def calculate_actual_distance(self): distance = 0.0 @@ -118,36 +118,40 @@ def calculate_actual_distance(self): @staticmethod def get_distance_btw_points(point_arr_1, point_arr_2): - return math.sqrt((point_arr_2[0] - point_arr_1[0]) ** 2 + (point_arr_2[1] - point_arr_1[1]) ** 2 + ( - point_arr_2[2] - point_arr_1[2]) ** 2) + return math.sqrt((point_arr_2[0] - point_arr_1[0]) ** 2 + + (point_arr_2[1] - point_arr_1[1]) ** 2 + + (point_arr_2[2] - point_arr_1[2]) ** 2) def draw_trace_3d(self): - graph_dir = self.get_graph_dir() + # Construct folder path + folder_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/" est_actual = self.est_position_array # obj_actual = self.obj_position_array radius = self.mission.radius height = self.mission.altitude + if not self.breach_flag: title = f"{self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" else: title = f"(FAILED) {self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" + center = [self.mission.center.x_val, self.mission.center.y_val, height] theta = np.linspace(0, 2 * np.pi, 100) x = center[0] + radius * np.cos(theta) y = center[1] + radius * np.sin(theta) z = np.ones(100) * height - planned = [] - for i in range(len(x)): - planned.append([x[i], y[i], -z[i]]) - - ThreeDimensionalGrapher.draw_trace_vs_planned(planed_position_list=planned, - actual_position_list=est_actual, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title) - ThreeDimensionalGrapher.draw_interactive_trace_vs_planned(planed_position_list=planned, - actual_position_list=est_actual, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title) + planned = [[x[i], y[i], -z[i]] for i in range(len(x))] + + # Use the grapher to draw and upload graphs + grapher = ThreeDimensionalGrapher(self.storage_service) + grapher.draw_trace_vs_planned(planed_position_list=planned, + actual_position_list=est_actual, + drone_name=self.target_drone, + title=title, + folder_path=folder_path) + grapher.draw_interactive_trace_vs_planned(planed_position_list=planned, + actual_position_list=est_actual, + drone_name=self.target_drone, + title=title, + folder_path=folder_path) diff --git a/backend/PythonClient/multirotor/monitor/drift_monitor.py b/backend/PythonClient/multirotor/monitor/drift_monitor.py index a0c713dc8..37eca548e 100644 --- a/backend/PythonClient/multirotor/monitor/drift_monitor.py +++ b/backend/PythonClient/multirotor/monitor/drift_monitor.py @@ -29,7 +29,7 @@ def start(self): def make_drifted_array(self): dt = self.dt - closest = 9223372036854775807 # Max int + closest = float('inf') # Maximum distance self.reached = False self.est_position_array = [] while self.mission.state != self.mission.State.END: @@ -54,26 +54,32 @@ def make_drifted_array(self): f"{round(self.threshold, 2)} meters. Closest distance: {round(closest, 2)} meters") def draw_trace_3d(self): + # Construct folder path for storage service + folder_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/" + actual = self.est_position_array dest = self.mission.point + + # Determine the title based on whether the target was reached if self.reached: title = f"Drift path\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}\n" \ - f"Closest distance: {round(self.closest,2)} meters" + f"Closest distance: {round(self.closest, 2)} meters" else: title = f"(FAILED) Drift path\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}\n" \ - f"Closest distance: {round(self.closest,2)} meters" - graph_dir = self.get_graph_dir() - grapher = ThreeDimensionalGrapher() + f"Closest distance: {round(self.closest, 2)} meters" + + # Use the grapher to draw and upload graphs + grapher = ThreeDimensionalGrapher(self.storage_service) grapher.draw_trace_vs_point(destination_point=dest, actual_position_list=actual, - full_target_directory=graph_dir, drone_name=self.target_drone, - title=title) + title=title, + folder_path=folder_path) grapher.draw_interactive_trace_vs_point(actual_position=actual, destination=dest, - full_target_directory=graph_dir, drone_name=self.target_drone, - title=title) + title=title, + folder_path=folder_path) if __name__ == "__main__": diff --git a/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py b/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py index 334329e28..b16e05bdf 100644 --- a/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py +++ b/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py @@ -124,22 +124,29 @@ def calculate_actual_distance(self): return distance def draw_trace_3d(self): - graph_dir = self.get_graph_dir() + # Construct the folder path + folder_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/" + + # Ensure the title reflects the mission status if not self.breach_flag: title = f"{self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" else: title = f"(FAILED) {self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" - grapher = ThreeDimensionalGrapher() - grapher.draw_trace_vs_planned(planed_position_list=self.mission.points, - actual_position_list=self.est_position_array, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title - ) - - grapher.draw_interactive_trace_vs_planned(planed_position_list=self.mission.points, - actual_position_list=self.est_position_array, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title - ) + + # Draw and upload the graphs + grapher = ThreeDimensionalGrapher(self.storage_service) + grapher.draw_trace_vs_planned( + planed_position_list=self.mission.points, + actual_position_list=self.est_position_array, + drone_name=self.target_drone, + title=title, + folder_path=folder_path + ) + + grapher.draw_interactive_trace_vs_planned( + planed_position_list=self.mission.points, + actual_position_list=self.est_position_array, + drone_name=self.target_drone, + title=title, + folder_path=folder_path + ) \ No newline at end of file diff --git a/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py b/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py index 447ca9447..7a90cb6d0 100644 --- a/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py +++ b/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py @@ -2,24 +2,19 @@ from matplotlib import pyplot as plt import matplotlib import plotly.express as px -import os import threading +import io # For in-memory buffer -# create a lock object - +# Create a lock object lock = threading.Lock() -matplotlib.use('agg') - - -def setup_dir(dir): - if not os.path.exists(dir): - os.makedirs(dir) +matplotlib.use('agg') # Use non-interactive backend class ThreeDimensionalGrapher: + def __init__(self, storage_service): + self.storage_service = storage_service - @staticmethod - def draw_trace(actual_position_list, full_target_directory, drone_name, title): + def draw_trace(self, actual_position_list, drone_name, title, folder_path): with lock: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') @@ -27,60 +22,55 @@ def draw_trace(actual_position_list, full_target_directory, drone_name, title): y1 = [point[1] for point in actual_position_list] z1 = [-point[2] for point in actual_position_list] ax.plot(x1, y1, z1, label="Position trace") - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # ax.set_xlim([-max_val, max_val]) - # ax.set_ylim([-max_val, max_val]) - # ax.set_zlim([-max_val, max_val]) ax.legend() ax.set_box_aspect([1, 1, 1]) ax.set_xlabel('North (+X) axis') ax.set_ylabel('East (+Y) axis') ax.set_zlabel('Height (+Z) axis') - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_plot.png") - # print(file_name) plt.title(title) - plt.savefig(file_name, dpi=200, bbox_inches='tight') + # Save to in-memory buffer + buf = io.BytesIO() + plt.savefig(buf, format='png', dpi=200, bbox_inches='tight') + buf.seek(0) + content = buf.read() + buf.close() plt.close() fig.clf() + # Upload to storage service + file_name = f"{folder_path}{drone_name}_plot.png" + self.storage_service.upload_to_service(file_name, content, content_type='image/png') - @staticmethod - def draw_trace_vs_planned(planed_position_list, actual_position_list, full_target_directory, drone_name, - title): + def draw_trace_vs_planned(self, planed_position_list, actual_position_list, drone_name, title, folder_path): with lock: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') x1 = [point[0] for point in planed_position_list] y1 = [point[1] for point in planed_position_list] z1 = [-point[2] for point in planed_position_list] - ax.plot(x1, y1, z1, label="Planed") - + ax.plot(x1, y1, z1, label="Planned") x2 = [point[0] for point in actual_position_list] y2 = [point[1] for point in actual_position_list] z2 = [-point[2] for point in actual_position_list] ax.plot(x2, y2, z2, label="Actual") - # ax.set_box_aspect([1, 1, 1]) - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # max_val = max(max_val, max(abs(max(x2, key=abs)), abs(max(y2, key=abs)), abs(max(z2, key=abs)))) ax.set_box_aspect([1, 1, 1]) - # ax.set_xlim([-max_val, max_val]) - # ax.set_ylim([-max_val, max_val]) - # ax.set_zlim([-max_val, max_val]) ax.set_xlabel('North (+X) axis') ax.set_ylabel('East (+Y) axis') ax.set_zlabel('Height (+Z) axis') ax.legend() - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_plot.png") - # print(file_name) plt.title(title) - plt.savefig(file_name, dpi=200, bbox_inches='tight') + # Save to in-memory buffer + buf = io.BytesIO() + plt.savefig(buf, format='png', dpi=200, bbox_inches='tight') + buf.seek(0) + content = buf.read() + buf.close() plt.close() fig.clf() + # Upload to storage service + file_name = f"{folder_path}{drone_name}_plot.png" + self.storage_service.upload_to_service(file_name, content, content_type='image/png') - @staticmethod - def draw_trace_vs_point(destination_point, actual_position_list, full_target_directory, drone_name, - title): + def draw_trace_vs_point(self, destination_point, actual_position_list, drone_name, title, folder_path): with lock: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') @@ -88,112 +78,100 @@ def draw_trace_vs_point(destination_point, actual_position_list, full_target_dir y1 = destination_point[1] z1 = -destination_point[2] ax.plot(x1, y1, z1, marker="o", markersize=10, label="Destination") - x2 = [point[0] for point in actual_position_list] y2 = [point[1] for point in actual_position_list] z2 = [-point[2] for point in actual_position_list] - ax.plot(x2, y2, z2, label="Actual") - - # point_max = max(destination_point, key=abs) - # max_val = max(max( - # abs(max(x2, key=abs)), abs(max(y2, key=abs)), abs(max(z2, key=abs)))) - # max_val = max(max_val, point_max) - # - # ax.set_xlim([-max_val, max_val]) - # ax.set_ylim([-max_val, max_val]) - # ax.set_zlim([-max_val, max_val]) ax.set_xlabel('North (+X) axis') ax.set_ylabel('East (+Y) axis') ax.set_zlabel('Height (+Z) axis') ax.set_box_aspect([1, 1, 1]) ax.legend() - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_plot.png") plt.title(title) - plt.savefig(file_name, dpi=200, bbox_inches='tight') + # Save to in-memory buffer + buf = io.BytesIO() + plt.savefig(buf, format='png', dpi=200, bbox_inches='tight') + buf.seek(0) + content = buf.read() + buf.close() plt.close() fig.clf() + # Upload to storage service + file_name = f"{folder_path}{drone_name}_plot.png" + self.storage_service.upload_to_service(file_name, content, content_type='image/png') - @staticmethod - def draw_interactive_trace(actual_position, full_target_directory, drone_name, - title): + def draw_interactive_trace(self, actual_position, drone_name, title, folder_path): with lock: - actual = actual_position - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - x1 = [point[0] for point in actual] - y1 = [point[1] for point in actual] - z1 = [-point[2] for point in actual] + x1 = [point[0] for point in actual_position] + y1 = [point[1] for point in actual_position] + z1 = [-point[2] for point in actual_position] fig = px.scatter_3d(title=title) fig.add_scatter3d(x=x1, y=y1, z=z1, name=drone_name + " path") - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # fig.update_layout(title_text=title, - # scene=dict(xaxis_range=[-max_val, max_val], - # yaxis_range=[-max_val, max_val], - # zaxis_range=[-max_val, max_val])) - setup_dir(full_target_directory) - ax.set_xlabel('North (+X) axis') - ax.set_ylabel('East (+Y) axis') - ax.set_zlabel('Height (+Z) axis') - file_name = os.path.join(full_target_directory, str(drone_name) + "_interactive.html") - fig.write_html(file_name) - plt.close() - - @staticmethod - def draw_interactive_trace_vs_point(destination, actual_position, full_target_directory, drone_name, - title): + fig.update_layout( + scene=dict( + xaxis_title='North (+X) axis', + yaxis_title='East (+Y) axis', + zaxis_title='Height (+Z) axis', + aspectratio=dict(x=1, y=1, z=1) + ) + ) + # Save to HTML in-memory buffer + html_str = fig.to_html() + content = html_str.encode('utf-8') + # Upload to storage service + file_name = f"{folder_path}{drone_name}_interactive.html" + self.storage_service.upload_to_service(file_name, content, content_type='text/html') + + def draw_interactive_trace_vs_point(self, destination, actual_position, drone_name, title, folder_path): with lock: - actual = actual_position - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - x1 = [point[0] for point in actual] - y1 = [point[1] for point in actual] - z1 = [-point[2] for point in actual] - df = pd.DataFrame({'x': x1, 'y': y1, 'z': z1}) - fig = px.scatter_3d(df, x='x', y='y', z='z') - ax.set_xlabel('North (+X) axis') - ax.set_ylabel('East (+Y) axis') - ax.set_zlabel('Height (+Z) axis') - fig.add_scatter3d(x=[destination[0]], y=[destination[1]], z=[-destination[2]], name="Destination point", - marker=dict(size=10, symbol='circle')) - # max_val = max(abs(max(destination, key=abs))) - # max_val = max(max_val, max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs)))) - # fig.update_layout(scene=dict(xaxis_range=[-max_val, max_val], - # yaxis_range=[-max_val, max_val], - # zaxis_range=[-max_val, max_val])) - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_interactive.html") - fig.write_html(file_name) - plt.close() - - @staticmethod - def draw_interactive_trace_vs_planned(planed_position_list, actual_position_list, full_target_directory, - drone_name, - title): + x1 = [point[0] for point in actual_position] + y1 = [point[1] for point in actual_position] + z1 = [-point[2] for point in actual_position] + fig = px.scatter_3d(x=x1, y=y1, z=z1, title=title) + fig.add_scatter3d( + x=[destination[0]], + y=[destination[1]], + z=[-destination[2]], + name="Destination point", + marker=dict(size=10, symbol='circle') + ) + fig.update_layout( + scene=dict( + xaxis_title='North (+X) axis', + yaxis_title='East (+Y) axis', + zaxis_title='Height (+Z) axis', + aspectratio=dict(x=1, y=1, z=1) + ) + ) + # Save to HTML in-memory buffer + html_str = fig.to_html() + content = html_str.encode('utf-8') + # Upload to storage service + file_name = f"{folder_path}{drone_name}_interactive.html" + self.storage_service.upload_to_service(file_name, content, content_type='text/html') + + def draw_interactive_trace_vs_planned(self, planed_position_list, actual_position_list, drone_name, title, folder_path): with lock: - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') x1 = [point[0] for point in actual_position_list] y1 = [point[1] for point in actual_position_list] z1 = [-point[2] for point in actual_position_list] x2 = [point[0] for point in planed_position_list] y2 = [point[1] for point in planed_position_list] z2 = [-point[2] for point in planed_position_list] - ax.set_xlabel('North (+X) axis') - ax.set_ylabel('East (+Y) axis') - ax.set_zlabel('Height (+Z) axis') - fig = px.scatter_3d(title=title) fig.add_scatter3d(x=x1, y=y1, z=z1, name="Actual") fig.add_scatter3d(x=x2, y=y2, z=z2, name="Planned") - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # max_val = max(max_val, max(abs(max(x2, key=abs)), abs(max(y2, key=abs)), abs(max(z2, key=abs)))) - # fig.update_layout(title_text=title, - # scene=dict(xaxis_range=[-max_val, max_val], - # yaxis_range=[-max_val, max_val], - # zaxis_range=[-max_val, max_val])) - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_interactive.html") - fig.write_html(file_name) - plt.close() + fig.update_layout( + scene=dict( + xaxis_title='North (+X) axis', + yaxis_title='East (+Y) axis', + zaxis_title='Height (+Z) axis', + aspectratio=dict(x=1, y=1, z=1) + ) + ) + # Save to HTML in-memory buffer + html_str = fig.to_html() + content = html_str.encode('utf-8') + # Upload to storage service + file_name = f"{folder_path}{drone_name}_interactive.html" + self.storage_service.upload_to_service(file_name, content, content_type='text/html')