From f670d365537c366bf84d85345930c11e4e2a1d0c Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Thu, 2 May 2024 17:52:55 +0200 Subject: [PATCH 1/2] Add efficiency pie chart, still need to define the service day, which is cyclical --- rssched/cli.py | 3 +- rssched/visualization/colors.py | 9 ++ rssched/visualization/fleet_efficiency.py | 89 ++++++++++++++++++++ rssched/visualization/plot.py | 2 + rssched/visualization/vehicle_utilization.py | 4 +- 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 rssched/visualization/fleet_efficiency.py diff --git a/rssched/cli.py b/rssched/cli.py index 89e0b33..9d62c41 100644 --- a/rssched/cli.py +++ b/rssched/cli.py @@ -3,7 +3,7 @@ import typer from typer import Argument, echo - +import time from rssched.io.reader import import_response from rssched.visualization.plot import generate_plots @@ -16,6 +16,7 @@ def main(source: Annotated[Path, Argument()]): response = import_response(source) for fig in generate_plots(response, source.stem): fig.show() + time.sleep(0.5) if __name__ == "__main__": diff --git a/rssched/visualization/colors.py b/rssched/visualization/colors.py index 848dbc6..2c40995 100644 --- a/rssched/visualization/colors.py +++ b/rssched/visualization/colors.py @@ -2,4 +2,13 @@ "ServiceTrip": "rgb(183, 183, 183)", "MaintenanceSlot": "rgb(45, 50, 125)", "DeadHeadTrip": "rgb(235, 0, 0)", + "Idle": "rgb(252, 187, 0)", } + +COLORS = [ + "rgb(20, 58, 133)", + "rgb(111, 34, 130)", + "rgb(232, 78, 16)", + "rgb(255, 222, 21)", + "rgb(0, 151, 59)", +] diff --git a/rssched/visualization/fleet_efficiency.py b/rssched/visualization/fleet_efficiency.py new file mode 100644 index 0000000..3ae7cef --- /dev/null +++ b/rssched/visualization/fleet_efficiency.py @@ -0,0 +1,89 @@ +import pandas as pd +import plotly.graph_objects as go + +from rssched.model.response import Response +from rssched.visualization.colors import EVENT_TYPES + + +def summarize_vehicle_activities(response): + activities = [] + + for fleet in response.schedule.fleet: + vehicle_type = fleet.vehicle_type + + for vehicle in fleet.vehicles: + vehicle_id = vehicle.id + times = [] + + for segment in vehicle.departure_segments: + times.append(("ServiceTrip", segment.departure, segment.arrival)) + for slot in vehicle.maintenance_slots: + times.append(("MaintenanceSlot", slot.start, slot.end)) + for trip in vehicle.dead_head_trips: + times.append(("DeadHeadTrip", trip.departure, trip.arrival)) + + times.sort(key=lambda x: x[1]) + + last_end_time = None + + for activity_type, start, end in times: + duration = (end - start).total_seconds() / 3600 + activities.append( + { + "vehicle_id": vehicle_id, + "vehicle_type": vehicle_type, + "activity_type": activity_type, + "duration": duration, + "start_time": start, + "end_time": end, + } + ) + + if last_end_time is not None and start > last_end_time: + idle_duration = (start - last_end_time).total_seconds() / 3600 + activities.append( + { + "vehicle_id": vehicle_id, + "vehicle_type": vehicle_type, + "activity_type": "Idle", + "duration": idle_duration, + "start_time": last_end_time, + "end_time": start, + } + ) + + last_end_time = end + + df_activities = pd.DataFrame(activities) + return df_activities + + +def plot_fleet_efficiency(response: Response, instance_name: str): + df = summarize_vehicle_activities(response) + + vehicle_types = df["vehicle_type"].unique() + + figs = [] + + for v_type in vehicle_types: + filtered_df = df[df["vehicle_type"] == v_type] + grouped = filtered_df.groupby("activity_type")["duration"].sum().reset_index() + fig = go.Figure( + data=[ + go.Pie( + labels=grouped["activity_type"], + values=grouped["duration"], + textinfo="label+percent", + insidetextorientation="radial", + marker_colors=[ + EVENT_TYPES[atype] for atype in grouped["activity_type"] + ], + ) + ] + ) + fig.update_layout( + title=f"Activity Distribution: {v_type} (instance: {instance_name})" + ) + figs.append(fig) + + return figs diff --git a/rssched/visualization/plot.py b/rssched/visualization/plot.py index c073691..7f77a23 100644 --- a/rssched/visualization/plot.py +++ b/rssched/visualization/plot.py @@ -1,5 +1,6 @@ from rssched.model.response import Response from rssched.visualization.active_events import plot_active_events_over_time +from rssched.visualization.fleet_efficiency import plot_fleet_efficiency from rssched.visualization.vehicle_type_gantt import plot_gantt_per_vehicle_type from rssched.visualization.vehicle_utilization import plot_vehicle_utilization @@ -8,4 +9,5 @@ def generate_plots(response: Response, instance_name: str): figures = plot_gantt_per_vehicle_type(response, instance_name) figures.append(plot_active_events_over_time(response, instance_name)) figures.append(plot_vehicle_utilization(response, instance_name)) + figures.extend(plot_fleet_efficiency(response, instance_name)) return figures diff --git a/rssched/visualization/vehicle_utilization.py b/rssched/visualization/vehicle_utilization.py index 30f5157..1a8d878 100644 --- a/rssched/visualization/vehicle_utilization.py +++ b/rssched/visualization/vehicle_utilization.py @@ -2,6 +2,7 @@ import plotly.express as px from rssched.model.response import Response +from rssched.visualization.colors import COLORS def plot_vehicle_utilization(response: Response, instance_name: str): @@ -28,11 +29,12 @@ def plot_vehicle_utilization(response: Response, instance_name: str): y="Duration", color="Vehicle Type", title=f"Vehicle Utilization (instance: {instance_name})", + color_discrete_sequence=COLORS, labels={"Duration": "Total Hours"}, ) fig.update_layout( xaxis_title="Vehicle ID", - yaxis_title="Total Utilization [h]", + yaxis_title="Service Trip Time [h]", xaxis={"categoryorder": "total descending"}, ) fig.update_layout(hovermode="x", hoverdistance=50) From ec3e9a49006faa59881574d6666d3eabf38f56f2 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Mon, 6 May 2024 14:37:39 +0200 Subject: [PATCH 2/2] Fix tests --- tests/visualization/test_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/visualization/test_plot.py b/tests/visualization/test_plot.py index 0616c87..4b34253 100644 --- a/tests/visualization/test_plot.py +++ b/tests/visualization/test_plot.py @@ -6,4 +6,4 @@ def test_generate_plots(): response = import_response(PkgDataAccess.locate_response()) figs = generate_plots(response, "test_instance") - assert len(figs) == 4 + assert len(figs) == 6