Skip to content

Commit

Permalink
Merge pull request #212 from rl-institut/feature/#210_extended_output…
Browse files Browse the repository at this point in the history
…_plots_v2

Extended output plots v2
  • Loading branch information
j-brendel authored Sep 19, 2024
2 parents b14045d + c7b808e commit 008e40a
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 9 deletions.
188 changes: 179 additions & 9 deletions simba/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
import re
from typing import Iterable

import matplotlib.colors
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.patches import Patch
from spice_ev.report import aggregate_global_results, plot, generate_reports, aggregate_timeseries
from spice_ev.util import sanitize

from simba import util

DPI = 300


def open_for_csv(file_path):
""" Create a file handle to write to.
Expand Down Expand Up @@ -177,11 +184,11 @@ def generate_plots(schedule, scenario, args):
file_path_png = args.results_directory / "run_overview.png"
if vars(args).get("scenario_name"):
file_path_png = file_path_png.with_stem(file_path_png.stem + '_' + args.scenario_name)
plt.savefig(file_path_png)
plt.savefig(file_path_png, dpi=DPI)
file_path_pdf = args.results_directory / "run_overview.pdf"
if vars(args).get("scenario_name"):
file_path_pdf = file_path_pdf.with_stem(file_path_pdf.stem + '_' + args.scenario_name)
plt.savefig(file_path_pdf)
plt.savefig(file_path_pdf, dpi=DPI)
if args.show_plots:
plt.show()

Expand All @@ -191,6 +198,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)
Expand Down Expand Up @@ -458,7 +468,7 @@ def plot_distance_per_rotation_distribution(extended_plots_path, schedule):
ax.set_title('Distribution of rotation length per vehicle type')
ax.legend()
plt.tight_layout()
plt.savefig(extended_plots_path / "distribution_distance.png")
plt.savefig(extended_plots_path / "distribution_distance.png", dpi=DPI)
plt.close()


Expand Down Expand Up @@ -488,7 +498,7 @@ def plot_consumption_per_rotation_distribution(extended_plots_path, schedule):
ax.set_title('Distribution of energy consumption of rotations per vehicle type')
ax.legend()
plt.tight_layout()
plt.savefig(extended_plots_path / "distribution_consumption")
plt.savefig(extended_plots_path / "distribution_consumption", dpi=DPI)
plt.close()


Expand Down Expand Up @@ -543,7 +553,7 @@ def plot_charge_type_distribution(extended_plots_path, scenario, schedule):
ax.yaxis.get_major_locator().set_params(integer=True)
ax.legend(["successful rotations", "negative rotations"])
ax.set_title("Feasibility of rotations per charging type")
plt.savefig(extended_plots_path / "charge_types")
plt.savefig(extended_plots_path / "charge_types", dpi=DPI)
plt.close()


Expand Down Expand Up @@ -622,13 +632,173 @@ 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.tight_layout()
plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_overview.png")
plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_overview.png", dpi=DPI)
plt.close(fig)


def ColorGenerator(vehicle_types):
# 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 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)
value = hsv[-1]
if rotation.charging_type == "depb":
if value >= 0.5:
value -= 0.3
elif rotation.charging_type == "oppb":
if value < 0.5:
value += 0.3
else:
raise NotImplementedError
hsv[-1] = value
color = matplotlib.colors.hsv_to_rgb(hsv)


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 VehicleIdRowGenerator():
# 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

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():
# create instances of the generators
row_generator = VehicleIdRowGenerator()
color_generator = ColorGenerator(schedule.vehicle_types)

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_generator, row_generator)
ax.set_ylabel("Vehicle ID")
# Vehicle ids need small fontsize to fit
ax.tick_params(axis='y', labelsize=6)
# add legend
handles = []
vts = {f"{rotation.vehicle_type}_{rotation.charging_type}": rotation
for rotation in sorted_rotations}
for key, rot in vts.items():
handles.append(Patch(color=color_generator.send(rot), label=key))
# Position legend at the top outside of the plot
ax.legend(handles=handles, loc='lower center', bbox_to_anchor=(0.5, 1),
ncol=len(handles)//2+1, prop={"size": 7})
fig.tight_layout()
# PDF so Block names stay readable
fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.pdf")
fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.png", dpi=DPI)
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 DenseRowGenerator():
# 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()
output_path_folder = output_path / "block_distribution"
output_path_folder.mkdir(parents=True, exist_ok=True)
for depot, rotations in rotations_per_depot.items():
# create instances of the generators
row_generator = DenseRowGenerator()
color_generator = ColorGenerator(schedule.vehicle_types)
# sort by departure_time and longest duration
sorted_rotations = list(
sorted(rotations,
key=lambda r: (r.departure_time, -(r.departure_time - r.arrival_time))))
fig, ax = create_plot_blocks(sorted_rotations, color_generator, row_generator)
ax.set_ylabel("Block")
ax.yaxis.get_major_locator().set_params(integer=True)
# add legend
handles = []
vts = {f"{rotation.vehicle_type}_{rotation.charging_type}": rotation
for rotation in sorted_rotations}
for key, rot in vts.items():
handles.append(Patch(color=color_generator.send(rot), label=key))
# Position legend at the top outside of the plot
ax.legend(handles=handles, loc='lower center', bbox_to_anchor=(0.5, 1),
ncol=len(handles)//2+1, prop={"size": 7})

fig.tight_layout()
# PDF so Block names stay readable
fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.pdf")
fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.png", dpi=DPI)
plt.close(fig)


def create_plot_blocks(sorted_rotations, color_generator, row_generator):
# initialize row_generator by yielding None
next(row_generator)
next(color_generator)
y_size = len(sorted_rotations) * 0.02 + 1.5
fig, ax = plt.subplots(figsize=(7, y_size))
for rotation in sorted_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', fontsize=2)

# Large heights create too much margin with default value of margins: Reduce value to 0.01
ax.axes.margins(y=0.01)
ax.grid(axis='x')
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%D %H:%M'))
ax.set_xlim(min(r.departure_time for r in sorted_rotations) - datetime.timedelta(minutes=30),
max(r.arrival_time for r in sorted_rotations) + datetime.timedelta(minutes=30))
ax.tick_params(axis='x', rotation=30)
return fig, ax


def count_active_rotations(scenario, schedule):
num_active_rotations = [0] * scenario.n_intervals
for rotation in schedule.rotations.values():
Expand Down Expand Up @@ -665,5 +835,5 @@ def plot_active_rotations(extended_plots_path, scenario, schedule):
plt.grid(axis="y")
plt.title("Active Rotations")
plt.tight_layout()
plt.savefig(extended_plots_path / "active_rotations")
plt.savefig(extended_plots_path / "active_rotations", dpi=DPI)
plt.close()
13 changes: 13 additions & 0 deletions simba/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 008e40a

Please sign in to comment.