diff --git a/simba/report.py b/simba/report.py index a44c7d1..000a1c0 100644 --- a/simba/report.py +++ b/simba/report.py @@ -6,10 +6,14 @@ import re from typing import Iterable +import matplotlib.colors import matplotlib.pyplot as plt +import matplotlib.dates as mdates from spice_ev.report import aggregate_global_results, plot, generate_reports, aggregate_timeseries from spice_ev.util import sanitize +from simba import util + def open_for_csv(file_path): """ Create a file handle to write to. @@ -191,6 +195,9 @@ def generate_plots(schedule, scenario, args): extended_plots_path = args.results_directory.joinpath("extended_plots") extended_plots_path.mkdir(parents=True, exist_ok=True) + plot_blocks_dense(schedule, extended_plots_path) + plot_vehicle_services(schedule, extended_plots_path) + plot_consumption_per_rotation_distribution(extended_plots_path, schedule) plot_distance_per_rotation_distribution(extended_plots_path, schedule) plot_charge_type_distribution(extended_plots_path, scenario, schedule) @@ -596,12 +603,151 @@ def plot_gc_power_timeseries(extended_plots_path, scenario): # xaxis are datetime strings ax.set_xlim(time_values[0], time_values[-1]) - ax.tick_params(axis='x', rotation=30) + # ax.tick_params(axis='x', rotation=30) plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_time_overview.png") plt.close(fig) +def plot_vehicle_services(schedule, output_path): + """Plots the rotations serviced by the same vehicle + + :param schedule: Provides the schedule data. + :type schedule; simba.schedule.Schedule + :param output_path: Path to the output folder + :type output_path: pathlib.Path + """ + + # find depots for every rotation + all_rotations = schedule.rotations + rotations_per_depot = {r.departure_name: [] for r in all_rotations.values()} + for rotation in all_rotations.values(): + rotations_per_depot[rotation.departure_name].append(rotation) + + def color_generator(): + # generates color according to vehicle_type and charging type of rotation + colors = util.cycling_generator(plt.rcParams['axes.prop_cycle'].by_key()["color"]) + color_per_vt = {vt: next(colors) for vt in schedule.vehicle_types} + color = None + while True: + rotation = yield color + color = color_per_vt[rotation.vehicle_type] + rgb = matplotlib.colors.to_rgb(color) + hsv = matplotlib.colors.rgb_to_hsv(rgb) + if rotation.charging_type == "depb": + pass + elif rotation.charging_type == "oppb": + hsv[-1] /= 2 + else: + raise NotImplementedError + color = matplotlib.colors.hsv_to_rgb(hsv) + + def vehicle_id_row_generator(): + # generate row ids by using the vehicle_id of the rotation + vehicle_id = None + while True: + rotation = yield vehicle_id + vehicle_id = rotation.vehicle_id + + # create instances of the generators + dense_row_generator = vehicle_id_row_generator() + color_gen = color_generator() + # initialize row_generator by yielding None + next(dense_row_generator) + next(color_gen) + + rotations_per_depot["All_Depots"] = all_rotations.values() + + output_path_folder = output_path / "vehicle_services" + output_path_folder.mkdir(parents=True, exist_ok=True) + for depot, rotations in rotations_per_depot.items(): + sorted_rotations = list( + sorted(rotations, + key=lambda x: (x.vehicle_type, x.charging_type, x.departure_time))) + fig, ax = create_plot_blocks(sorted_rotations, color_gen, dense_row_generator) + ax.set_xlabel("Vehicle ID") + ax.set_xlabel("Block") + fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.png") + plt.close(fig) + + +def plot_blocks_dense(schedule, output_path): + """Plots the different loads (total, feedin, external) of all grid connectors. + + :param schedule: Provides the schedule data. + :type schedule; simba.schedule.Schedule + :param output_path: Path to the output folder + :type output_path: pathlib.Path + """ + # find depots for every rotation + all_rotations = schedule.rotations + rotations_per_depot = {r.departure_name: [] for r in all_rotations.values()} + for rotation in all_rotations.values(): + rotations_per_depot[rotation.departure_name].append(rotation) + + def color_generator(): + # Generates color according to rotation vehicle_type + + colors = util.cycling_generator(plt.rcParams['axes.prop_cycle'].by_key()["color"]) + color_per_vt = {vt: next(colors) for vt in schedule.vehicle_types} + color = None + while True: + rotation = yield color + color = color_per_vt[rotation.vehicle_type] + + def dense_row_generator(): + # Generates row id by trying to find lowes row with a rotation which arrived + arrival_times = [] + row_nr = None + while True: + rotation = yield row_nr + for i in range(len(arrival_times)): + at = arrival_times[i] + if at <= rotation.departure_time: + arrival_times[i] = rotation.arrival_time + row_nr = i + break + else: + arrival_times.append(rotation.arrival_time) + row_nr = len(arrival_times) - 1 + + rotations_per_depot["All_Depots"] = all_rotations.values() + # create instances of the generators + dense_row_generator = dense_row_generator() + color_gen = color_generator() + # initialize row_generator by yielding None + next(dense_row_generator) + next(color_gen) + output_path_folder = output_path / "block_distribution" + output_path_folder.mkdir(parents=True, exist_ok=True) + for depot, rotations in rotations_per_depot.items(): + fig, ax = create_plot_blocks(rotations, color_gen, dense_row_generator) + ax.set_xlabel("Block") + fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.png") + plt.close(fig) + + +def create_plot_blocks(rotations, color_generator, row_generator): + fig, ax = plt.subplots() + rotations = list(sorted(rotations, key=lambda r: r.departure_time)) + for rotation in rotations: + row_nr = row_generator.send(rotation) + width = rotation.arrival_time - rotation.departure_time + artist = ax.barh([row_nr], width=[width], height=0.8, left=rotation.departure_time, + label=rotation.id, color=color_generator.send(rotation)) + ax.bar_label(artist, labels=[rotation.id], label_type='center') + + ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%D %H:%M')) + ax.yaxis.get_major_locator().set_params(integer=True) + ax.set_xlim(min(r.departure_time for r in rotations) - datetime.timedelta(minutes=10), + max(r.arrival_time for r in rotations) + datetime.timedelta(minutes=10)) + ax.set_xticklabels(ax.get_xticklabels(), rotation=30, ha='right') + ax.set_xlabel('Time') + fig.tight_layout() + return fig, ax + + def count_active_rotations(scenario, schedule): num_active_rotations = [0] * scenario.n_intervals for rotation in schedule.rotations.values(): diff --git a/simba/util.py b/simba/util.py index dcecc07..e2afdce 100644 --- a/simba/util.py +++ b/simba/util.py @@ -259,6 +259,19 @@ def cast_float_or_none(val: any) -> any: return None +def cycling_generator(cycle: []): + """Generator to loop over lists + :param cycle: list to cycle through + :type cycle: list() + :yields: iterated value + :rtype: Iterator[any] + """ + i = 0 + while True: + yield cycle[i % len(cycle)] + i += 1 + + def setup_logging(args, time_str): """ Setup logging.