From 12f0a0f916f59fc6ccfa420f370d4b5c45fc558e Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Wed, 18 Jan 2023 14:54:16 +0100 Subject: [PATCH 01/41] Add flag extended_plots and dummy functions #94 --- ebus_toolbox/report.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ebus_toolbox/report.py b/ebus_toolbox/report.py index f5f33419..a8f2e58b 100644 --- a/ebus_toolbox/report.py +++ b/ebus_toolbox/report.py @@ -92,7 +92,19 @@ def generate_gc_overview(schedule, scenario, args): *use_factors]) -def generate(schedule, scenario, args): +def bus_type_distribution_mileage_consumption(): + pass + + +def charge_type_proportion(): + pass + + +def gc_power_time_overview(): + pass + + +def generate(schedule, scenario, args, extended_plots=False): """Generates all output files/ plots and saves them in the output directory. :param schedule: Driving schedule for the simulation. @@ -101,8 +113,16 @@ def generate(schedule, scenario, args): :type scenario: spice_ev.Scenario :param args: Configuration arguments specified in config files contained in configs directory. :type args: argparse.Namespace + :param extended_plots: Generates more plots. + :type extended_plots: bool """ + # generate if needed extended output plots + if extended_plots: + bus_type_distribution_mileage_consumption() + charge_type_proportion() + gc_power_time_overview() + # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in spiceEV with warnings.catch_warnings(): warnings.simplefilter('ignore', UserWarning) From 4f58721910f04ed2acce57674e00ec28bd5dc5f1 Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Mon, 6 Feb 2023 11:28:54 +0100 Subject: [PATCH 02/41] Add new plot functions #94 --- ebus_toolbox/report.py | 79 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/ebus_toolbox/report.py b/ebus_toolbox/report.py index a8f2e58b..de9bde4a 100644 --- a/ebus_toolbox/report.py +++ b/ebus_toolbox/report.py @@ -92,12 +92,79 @@ def generate_gc_overview(schedule, scenario, args): *use_factors]) -def bus_type_distribution_mileage_consumption(): - pass +def bus_type_distribution_consumption_rotation(args, schedule): + """Plots distribution of bus types in consumption brackets as a stacked bar chart. + :param args: Configuration arguments from cfg file. args.output_directory is used + :type args: argparse.Namespace + :param schedule: Driving schedule for the simulation. schedule.rotations are used + :type schedule: eBus-Toolbox.Schedule + """ -def charge_type_proportion(): - pass + step = 50 + max_con = int(max([schedule.rotations[rot].consumption for rot in schedule.rotations])) + if max_con % step < step / 2: + max_con_up = ((max_con // step) * step) + else: + max_con_up = (max_con // step) * step + step + labels = [f"{i - step} - {i}" for i in range(step, int(max_con), step) if i > 0] + bins = {v_types: [0 for _ in range(int(max_con / step))] for v_types in schedule.vehicle_types} + + # fill bins with rotations + for rot in schedule.rotations: + for v_type in schedule.vehicle_types: + if schedule.rotations[rot].vehicle_type == v_type: + position = int(schedule.rotations[rot].consumption // step) + if position >= max_con_up / step: + position -= 1 + bins[v_type][position] += 1 + break + # plot + fig, ax = plt.subplots() + bar_bottom = [0 for _ in range(max_con_up//step)] + for v_type in schedule.vehicle_types: + ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) + # something more efficient than for loop + for i in range(max_con_up//step): + bar_bottom[i] += bins[v_type][i] + print(bar_bottom) + ax.set_xlabel('Energieverbrauch in kWh') + ax.set_ylabel('Anzahl der Umläufe') + ax.set_title('Verteilung der Bustypen über den Energieverbrauch und den Umläufen') + ax.legend() + fig.autofmt_xdate() + ax.yaxis.grid(True) + plt.savefig(args.output_directory / "distribution_bustypes_consumption_rotations") + + +def charge_type_proportion(args, schedule): + """Plots percentages of charging types in a horizontal bar chart. + + :param args: Configuration arguments from cfg file. args.output_directory is used + :type args: argparse.Namespace + :param schedule: Driving schedule for the simulation. schedule.rotations are used + :type schedule: eBus-Toolbox.Schedule + """ + # get plotting data + charging_types = {'oppb': 0, 'depb': 0, 'rest': 0} + for rot in schedule.rotations: + if schedule.rotations[rot].charging_type == 'oppb': + charging_types['oppb'] += schedule.rotations[rot].consumption + elif schedule.rotations[rot].charging_type == 'depb': + charging_types['depb'] += schedule.rotations[rot].consumption + else: + charging_types['rest'] += schedule.rotations[rot].consumption + bins = [round(v/sum(charging_types.values()) * 100, 1) for v in charging_types.values()] + + # plot + fig, ax = plt.subplots() + ax.barh([k for k in charging_types.keys()], bins, color=['#6495ED', '#66CDAA', 'grey']) + ax.set_xlabel('Umläufe') + ax.set_ylabel('Ladetyp') + ax.set_title('Verteilung von Gelegenheitslader, Depotlader und nicht elektrifizierbaren') + ax.bar_label(ax.containers[0], [f"{v}%" for v in bins]) + ax.yaxis.grid(True) + plt.savefig(args.output_directory / "charge_type_proportion") def gc_power_time_overview(): @@ -119,8 +186,8 @@ def generate(schedule, scenario, args, extended_plots=False): # generate if needed extended output plots if extended_plots: - bus_type_distribution_mileage_consumption() - charge_type_proportion() + bus_type_distribution_consumption_rotation(args, schedule) + charge_type_proportion(args, schedule) gc_power_time_overview() # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in spiceEV From 22e43bccd0fe77898a93d8abd8224587bb15ab14 Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Mon, 6 Mar 2023 10:50:34 +0100 Subject: [PATCH 03/41] Implement gc_power_time_overview #94 --- ebus_toolbox/report.py | 65 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/ebus_toolbox/report.py b/ebus_toolbox/report.py index de9bde4a..133e98d3 100644 --- a/ebus_toolbox/report.py +++ b/ebus_toolbox/report.py @@ -167,8 +167,61 @@ def charge_type_proportion(args, schedule): plt.savefig(args.output_directory / "charge_type_proportion") -def gc_power_time_overview(): - pass +def gc_power_time_overview_example(args, schedule, scenario): + + gc_list = list(scenario.constants.grid_connectors.keys()) + for gc in gc_list: + # data + ts = getattr(scenario, f"{gc}_timeseries") + time = ts["time"] + total = ts["grid power [kW]"] + feed_in = ts["feed-in [kW]"] + ext_load = ts["ext.load [kW]"] + cs = ts["sum CS power"] + + # plot + plt.plot(time, total, label="total") + plt.plot(time, feed_in, label="feed_in") + plt.plot(time, ext_load, label="ext_load") + plt.plot(time, cs, label="CS") + plt.legend() + plt.xticks(rotation=45) + + plt.savefig(args.output_directory / f"{gc}_power_time_overview") + plt.clf() + + +def gc_power_time_overview(args, schedule, scenario): + + gc_list = list(scenario.constants.grid_connectors.keys()) + for gc in gc_list: + # data + ts = [scenario.start_time if i == 0 else scenario.start_time+scenario.interval*i for i in range(scenario.n_intervals)] + total = scenario.totalLoad[gc] + feed_in = scenario.feedInPower[gc] + ext_load = [sum(v.values()) for v in scenario.extLoads[gc]] + + print(ext_load) + + #cs = [sum(int(j) for j in i) for i in scenario.connChargeByTS[gc]] + """for i in scenario.connChargeByTS[gc]: + for j in i: + print(j) + print()""" + + # plot + plt.plot(ts, total, label="total") + plt.plot(ts, feed_in, label="feed_in") + plt.plot(ts, ext_load, label="ext_load") + #plt.plot(ts, cs, label="CS") + plt.legend() + plt.xticks(rotation=45) + + gc = gc.replace("/", "") + gc = gc.replace(".", "") + plt.savefig(args.output_directory / f"{gc}_power_time_overview") + plt.clf() + plt.cla() def generate(schedule, scenario, args, extended_plots=False): @@ -185,10 +238,12 @@ def generate(schedule, scenario, args, extended_plots=False): """ # generate if needed extended output plots + extended_plots = True if extended_plots: - bus_type_distribution_consumption_rotation(args, schedule) - charge_type_proportion(args, schedule) - gc_power_time_overview() + # bus_type_distribution_consumption_rotation(args, schedule) + # charge_type_proportion(args, schedule) + gc_power_time_overview(args, schedule, scenario) + exit(0) # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in spiceEV with warnings.catch_warnings(): From 83c9734a3d8357b34b98984ee0d3343379d52bfd Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Mon, 6 Mar 2023 17:39:55 +0100 Subject: [PATCH 04/41] Replace percentage with total number of rotation and make vertical barchart in charge_type, improve gc_power_time #94 --- ebus_toolbox/report.py | 57 ++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/ebus_toolbox/report.py b/ebus_toolbox/report.py index 133e98d3..0e1b8b36 100644 --- a/ebus_toolbox/report.py +++ b/ebus_toolbox/report.py @@ -127,7 +127,6 @@ def bus_type_distribution_consumption_rotation(args, schedule): # something more efficient than for loop for i in range(max_con_up//step): bar_bottom[i] += bins[v_type][i] - print(bar_bottom) ax.set_xlabel('Energieverbrauch in kWh') ax.set_ylabel('Anzahl der Umläufe') ax.set_title('Verteilung der Bustypen über den Energieverbrauch und den Umläufen') @@ -149,20 +148,22 @@ def charge_type_proportion(args, schedule): charging_types = {'oppb': 0, 'depb': 0, 'rest': 0} for rot in schedule.rotations: if schedule.rotations[rot].charging_type == 'oppb': - charging_types['oppb'] += schedule.rotations[rot].consumption + charging_types['oppb'] += 1 elif schedule.rotations[rot].charging_type == 'depb': - charging_types['depb'] += schedule.rotations[rot].consumption + charging_types['depb'] += 1 else: - charging_types['rest'] += schedule.rotations[rot].consumption - bins = [round(v/sum(charging_types.values()) * 100, 1) for v in charging_types.values()] - + charging_types['rest'] += 1 # plot fig, ax = plt.subplots() - ax.barh([k for k in charging_types.keys()], bins, color=['#6495ED', '#66CDAA', 'grey']) - ax.set_xlabel('Umläufe') - ax.set_ylabel('Ladetyp') - ax.set_title('Verteilung von Gelegenheitslader, Depotlader und nicht elektrifizierbaren') - ax.bar_label(ax.containers[0], [f"{v}%" for v in bins]) + ax.bar( + [k for k in charging_types.keys()], + [v for v in charging_types.values()], + color=['#6495ED', '#66CDAA', 'grey'] + ) + ax.set_xlabel("Ladetyp") + ax.set_ylabel("Umläufe") + ax.set_title("Verteilung von Gelegenheitslader, Depotlader und nicht elektrifizierbaren") + ax.bar_label(ax.containers[0], [v for v in [v for v in charging_types.values()]]) ax.yaxis.grid(True) plt.savefig(args.output_directory / "charge_type_proportion") @@ -195,30 +196,17 @@ def gc_power_time_overview(args, schedule, scenario): gc_list = list(scenario.constants.grid_connectors.keys()) for gc in gc_list: - # data - ts = [scenario.start_time if i == 0 else scenario.start_time+scenario.interval*i for i in range(scenario.n_intervals)] - total = scenario.totalLoad[gc] - feed_in = scenario.feedInPower[gc] - ext_load = [sum(v.values()) for v in scenario.extLoads[gc]] - - print(ext_load) - - #cs = [sum(int(j) for j in i) for i in scenario.connChargeByTS[gc]] - """for i in scenario.connChargeByTS[gc]: - for j in i: - print(j) - print()""" - - # plot - plt.plot(ts, total, label="total") - plt.plot(ts, feed_in, label="feed_in") - plt.plot(ts, ext_load, label="ext_load") - #plt.plot(ts, cs, label="CS") + ts = [ + scenario.start_time if i == 0 else + scenario.start_time+scenario.interval*i for i in range(scenario.n_intervals) + ] + plt.plot(ts, scenario.totalLoad[gc], label="total") + plt.plot(ts, scenario.feedInPower[gc], label="feed_in") + plt.plot(ts, [sum(v.values()) for v in scenario.extLoads[gc]], label="ext_load") plt.legend() plt.xticks(rotation=45) - gc = gc.replace("/", "") - gc = gc.replace(".", "") + gc = gc.replace("/", "").replace(".", "") plt.savefig(args.output_directory / f"{gc}_power_time_overview") plt.clf() plt.cla() @@ -240,10 +228,9 @@ def generate(schedule, scenario, args, extended_plots=False): # generate if needed extended output plots extended_plots = True if extended_plots: - # bus_type_distribution_consumption_rotation(args, schedule) - # charge_type_proportion(args, schedule) + bus_type_distribution_consumption_rotation(args, schedule) + charge_type_proportion(args, schedule) gc_power_time_overview(args, schedule, scenario) - exit(0) # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in spiceEV with warnings.catch_warnings(): From c4540f5b4cafc00984d006ccd741589b06f16db4 Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Tue, 7 Mar 2023 10:45:56 +0100 Subject: [PATCH 05/41] Add docstring, change xticks rotation, add plt.close() #94 --- ebus_toolbox/report.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ebus_toolbox/report.py b/ebus_toolbox/report.py index 0e1b8b36..75602915 100644 --- a/ebus_toolbox/report.py +++ b/ebus_toolbox/report.py @@ -93,7 +93,7 @@ def generate_gc_overview(schedule, scenario, args): def bus_type_distribution_consumption_rotation(args, schedule): - """Plots distribution of bus types in consumption brackets as a stacked bar chart. + """Plots the distribution of bus types in consumption brackets as a stacked bar chart. :param args: Configuration arguments from cfg file. args.output_directory is used :type args: argparse.Namespace @@ -137,7 +137,7 @@ def bus_type_distribution_consumption_rotation(args, schedule): def charge_type_proportion(args, schedule): - """Plots percentages of charging types in a horizontal bar chart. + """Plots the absolute number of rotations distributed by charging types on a bar chart. :param args: Configuration arguments from cfg file. args.output_directory is used :type args: argparse.Namespace @@ -192,8 +192,14 @@ def gc_power_time_overview_example(args, schedule, scenario): plt.clf() -def gc_power_time_overview(args, schedule, scenario): +def gc_power_time_overview(args, scenario): + """Plots the different loads (total, feedin, external) of all grid connectors. + :param args: Configuration arguments from cfg file. args.output_directory is used + :type args: argparse.Namespace + :param scenario: Provides the data for the grid connectors over time. + :type scenario: spice_ev.Scenario + """ gc_list = list(scenario.constants.grid_connectors.keys()) for gc in gc_list: ts = [ @@ -204,12 +210,11 @@ def gc_power_time_overview(args, schedule, scenario): plt.plot(ts, scenario.feedInPower[gc], label="feed_in") plt.plot(ts, [sum(v.values()) for v in scenario.extLoads[gc]], label="ext_load") plt.legend() - plt.xticks(rotation=45) + plt.xticks(rotation=30) gc = gc.replace("/", "").replace(".", "") plt.savefig(args.output_directory / f"{gc}_power_time_overview") - plt.clf() - plt.cla() + plt.close() def generate(schedule, scenario, args, extended_plots=False): @@ -226,11 +231,10 @@ def generate(schedule, scenario, args, extended_plots=False): """ # generate if needed extended output plots - extended_plots = True if extended_plots: bus_type_distribution_consumption_rotation(args, schedule) charge_type_proportion(args, schedule) - gc_power_time_overview(args, schedule, scenario) + gc_power_time_overview(args, scenario) # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in spiceEV with warnings.catch_warnings(): From c7862ce10e3a24a1469b7c2d55421246f83ab437 Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Wed, 22 Mar 2023 13:47:24 +0100 Subject: [PATCH 06/41] Fix bug in bus_type_distribution, rename max_con_up to bin_number, add title and label to gc_power_time_overview #94 --- ebus_toolbox/report.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/ebus_toolbox/report.py b/ebus_toolbox/report.py index 75602915..5b57a5b0 100644 --- a/ebus_toolbox/report.py +++ b/ebus_toolbox/report.py @@ -102,30 +102,30 @@ def bus_type_distribution_consumption_rotation(args, schedule): """ step = 50 + # get bin_number max_con = int(max([schedule.rotations[rot].consumption for rot in schedule.rotations])) - if max_con % step < step / 2: - max_con_up = ((max_con // step) * step) + if max_con % step < step/2: + bin_number = ((max_con // step) * step) else: - max_con_up = (max_con // step) * step + step - labels = [f"{i - step} - {i}" for i in range(step, int(max_con), step) if i > 0] - bins = {v_types: [0 for _ in range(int(max_con / step))] for v_types in schedule.vehicle_types} + bin_number = (max_con // step) * step + step + labels = [f"{i - step} - {i}" for i in range(step, bin_number+step, step) if i > 0] + bins = {v_types: [0 for _ in range(bin_number // step)] for v_types in schedule.vehicle_types} # fill bins with rotations for rot in schedule.rotations: for v_type in schedule.vehicle_types: if schedule.rotations[rot].vehicle_type == v_type: position = int(schedule.rotations[rot].consumption // step) - if position >= max_con_up / step: - position -= 1 - bins[v_type][position] += 1 + if position >= bin_number/step: + position -= bin_number // step - 1 + bins[v_type][position] += 1 # index out of range break # plot fig, ax = plt.subplots() - bar_bottom = [0 for _ in range(max_con_up//step)] + bar_bottom = [0 for _ in range(bin_number//step)] for v_type in schedule.vehicle_types: ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) - # something more efficient than for loop - for i in range(max_con_up//step): + for i in range(bin_number//step): bar_bottom[i] += bins[v_type][i] ax.set_xlabel('Energieverbrauch in kWh') ax.set_ylabel('Anzahl der Umläufe') @@ -134,6 +134,7 @@ def bus_type_distribution_consumption_rotation(args, schedule): fig.autofmt_xdate() ax.yaxis.grid(True) plt.savefig(args.output_directory / "distribution_bustypes_consumption_rotations") + plt.close() def charge_type_proportion(args, schedule): @@ -166,6 +167,7 @@ def charge_type_proportion(args, schedule): ax.bar_label(ax.containers[0], [v for v in [v for v in charging_types.values()]]) ax.yaxis.grid(True) plt.savefig(args.output_directory / "charge_type_proportion") + plt.close() def gc_power_time_overview_example(args, schedule, scenario): @@ -211,6 +213,8 @@ def gc_power_time_overview(args, scenario): plt.plot(ts, [sum(v.values()) for v in scenario.extLoads[gc]], label="ext_load") plt.legend() plt.xticks(rotation=30) + plt.ylabel("Power in kW") + plt.title(f"Power: {gc}") gc = gc.replace("/", "").replace(".", "") plt.savefig(args.output_directory / f"{gc}_power_time_overview") From a328934304b2b92deacc344c3ad1e0844ba49d62 Mon Sep 17 00:00:00 2001 From: lilAndy-bruh Date: Wed, 13 Dec 2023 15:50:43 +0100 Subject: [PATCH 07/41] Correct code so old plots work, add argument extended_output_plots #94 --- data/examples/simba.cfg | 1 + simba/report.py | 286 ++++++++++++++++++++-------------------- simba/util.py | 2 + 3 files changed, 146 insertions(+), 143 deletions(-) diff --git a/data/examples/simba.cfg b/data/examples/simba.cfg index 3ec10b55..777d7496 100644 --- a/data/examples/simba.cfg +++ b/data/examples/simba.cfg @@ -49,6 +49,7 @@ check_rotation_consistency = false skip_inconsistent_rotations = false # Show plots for users to view, only valid if generate_report = true (default: false) show_plots = true +extended_output_plots = true # Rotation filter variable, options: # "include": include only the rotations from file 'rotation_filter' # "exclude": exclude the rotations from file 'rotation_filter' from the schedule diff --git a/simba/report.py b/simba/report.py index c216f76c..d4699fb1 100644 --- a/simba/report.py +++ b/simba/report.py @@ -7,6 +7,139 @@ from spice_ev.report import aggregate_global_results, plot, generate_reports +def generate(schedule, scenario, args): + """Generates all output files/ plots and saves them in the output directory. + + :param schedule: Driving schedule for the simulation. + :type schedule: simba.Schedule + :param scenario: Scenario for with to generate timeseries. + :type scenario: spice_ev.Scenario + :param args: Configuration arguments specified in config files contained in configs directory. + :type args: argparse.Namespace + """ + + # generate if needed extended output plots + if args.extended_output_plots: + bus_type_distribution_consumption_rotation(args, schedule) + charge_type_proportion(args, schedule) + gc_power_time_overview(args, scenario) + + # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in SpiceEV + # re-route output paths + args.save_soc = args.output_directory / "vehicle_socs.csv" + args.save_results = args.output_directory / "info.json" + args.save_timeseries = args.output_directory / "ts.csv" + generate_reports(scenario, vars(args).copy()) + args.save_timeseries = None + args.save_results = None + args.save_soc = None + + # generate gc power overview + generate_gc_power_overview_timeseries(scenario, args) + + # generate gc overview + generate_gc_overview(schedule, scenario, args) + + # save plots as png and pdf + generate_plots(scenario, args) + + # calculate SOCs for each rotation + rotation_infos = [] + + negative_rotations = schedule.get_negative_rotations(scenario) + + interval = datetime.timedelta(minutes=args.interval) + sim_start_time = schedule.get_departure_of_first_trip() + # rotations might be empty, start_time is None + if sim_start_time: + sim_start_time -= datetime.timedelta(minutes=args.signal_time_dif) + else: + sim_start_time = datetime.datetime.fromtimestamp(0) + + incomplete_rotations = [] + rotation_socs = {} + for id, rotation in schedule.rotations.items(): + # get SOC timeseries for this rotation + vehicle_id = rotation.vehicle_id + + # get soc timeseries for current rotation + vehicle_soc = scenario.vehicle_socs[vehicle_id] + start_idx = (rotation.departure_time - sim_start_time) // interval + end_idx = start_idx + ((rotation.arrival_time - rotation.departure_time) // interval) + if end_idx > scenario.n_intervals: + # SpiceEV stopped before rotation was fully simulated + incomplete_rotations.append(id) + continue + rotation_soc_ts = vehicle_soc[start_idx:end_idx] + + # bus does not return before simulation end + # replace trailing None values with last numeric value + for i, soc in enumerate(reversed(rotation_soc_ts)): + if soc is not None: + break + last_known_idx = len(rotation_soc_ts) - 1 - i + rotation_soc_ts[last_known_idx + 1:] = i * [rotation_soc_ts[last_known_idx]] + + rotation_info = { + "rotation_id": id, + "start_time": rotation.departure_time.isoformat(), + "end_time": rotation.arrival_time.isoformat(), + "vehicle_type": rotation.vehicle_type, + "vehicle_id": rotation.vehicle_id, + "depot_name": rotation.departure_name, + "lines": ':'.join(rotation.lines), + "total_consumption_[kWh]": rotation.consumption, + "distance": rotation.distance, + "charging_type": rotation.charging_type, + "SOC_at_arrival": rotation_soc_ts[-1], + "Minimum_SOC": min(rotation_soc_ts), + "Negative_SOC": 1 if id in negative_rotations else 0 + } + rotation_infos.append(rotation_info) + + # save SOCs for each rotation + rotation_socs[id] = [None] * scenario.n_intervals + rotation_socs[id][start_idx:end_idx] = rotation_soc_ts + + if incomplete_rotations: + logging.warning( + "SpiceEV stopped before simulation of the these rotations were completed:\n" + f"{', '.join(incomplete_rotations)}\n" + "Omit parameter to simulate entire schedule.") + + if rotation_infos: + with open(args.output_directory / "rotation_socs.csv", "w", newline='') as f: + csv_writer = csv.writer(f) + # order rotations naturally + rotations = sorted(rotation_socs.keys(), key=lambda k: int(k)) + csv_writer.writerow(["time"] + rotations) + for i in range(scenario.n_intervals): + t = sim_start_time + i * scenario.interval + socs = [str(rotation_socs[k][i]) for k in rotations] + csv_writer.writerow([str(t)] + socs) + + with open(args.output_directory / "rotation_summary.csv", "w", newline='') as f: + csv_writer = csv.DictWriter(f, list(rotation_infos[0].keys())) + csv_writer.writeheader() + csv_writer.writerows(rotation_infos) + + # summary of used vehicle types and all costs + if args.cost_calculation: + with open(args.output_directory / "summary_vehicles_costs.csv", "w", newline='') as f: + csv_writer = csv.writer(f) + csv_writer.writerow(["parameter", "value", "unit"]) + for key, value in schedule.vehicle_type_counts.items(): + if value > 0: + csv_writer.writerow([key, value, "vehicles"]) + for key, value in scenario.costs.items(): + if "annual" in key: + csv_writer.writerow([key, round(value, 2), "€/year"]) + else: + csv_writer.writerow([key, round(value, 2), "€"]) + + logging.info(f"Plots and output files saved in {args.output_directory}") + + def generate_gc_power_overview_timeseries(scenario, args): """Generate a csv timeseries with each grid connector's summed up charging station power @@ -21,7 +154,7 @@ def generate_gc_power_overview_timeseries(scenario, args): if not gc_list: return - with open(args.results_directory / "gc_power_overview_timeseries.csv", "w", newline='') as f: + with open(args.output_directory / "gc_power_overview_timeseries.csv", "w", newline='') as f: csv_writer = csv.writer(f) csv_writer.writerow(["time"] + gc_list) stations = [] @@ -53,7 +186,7 @@ def generate_gc_overview(schedule, scenario, args): used_gc_list = list(scenario.components.grid_connectors.keys()) stations = getattr(schedule, "stations") - with open(args.results_directory / "gc_overview.csv", "w", newline='') as f: + with open(args.output_directory / "gc_overview.csv", "w", newline='') as f: csv_writer = csv.writer(f) csv_writer.writerow(["station_name", "station_type", @@ -100,7 +233,7 @@ def generate_plots(scenario, args): :param scenario: Scenario to plot. :type scenario: spice_ev.Scenario - :param args: Configuration. Uses results_directory and show_plots. + :param args: Configuration. Uses output_directory and show_plots. :type args: argparse.Namespace """ aggregate_global_results(scenario) @@ -110,13 +243,14 @@ def generate_plots(scenario, args): plt.clf() plot(scenario) plt.gcf().set_size_inches(10, 10) - plt.savefig(args.results_directory / "run_overview.png") - plt.savefig(args.results_directory / "run_overview.pdf") + plt.savefig(args.output_directory / "run_overview.png") + plt.savefig(args.output_directory / "run_overview.pdf") if args.show_plots: plt.show() # revert logging override logging.disable(logging.NOTSET) + def bus_type_distribution_consumption_rotation(args, schedule): """Plots the distribution of bus types in consumption brackets as a stacked bar chart. @@ -227,15 +361,16 @@ def gc_power_time_overview(args, scenario): :param scenario: Provides the data for the grid connectors over time. :type scenario: spice_ev.Scenario """ - gc_list = list(scenario.constants.grid_connectors.keys()) + gc_list = list(scenario.components.grid_connectors.keys()) for gc in gc_list: ts = [ scenario.start_time if i == 0 else scenario.start_time+scenario.interval*i for i in range(scenario.n_intervals) ] plt.plot(ts, scenario.totalLoad[gc], label="total") - plt.plot(ts, scenario.feedInPower[gc], label="feed_in") - plt.plot(ts, [sum(v.values()) for v in scenario.extLoads[gc]], label="ext_load") + plt.plot(ts, scenario.localGenerationPower[gc], label="feed_in") + ext_loads = [total - pv for total, pv in zip(scenario.totalLoad[gc], scenario.localGenerationPower[gc])] + plt.plot(ts, ext_loads, label="ext_load") plt.legend() plt.xticks(rotation=30) plt.ylabel("Power in kW") @@ -244,138 +379,3 @@ def gc_power_time_overview(args, scenario): gc = gc.replace("/", "").replace(".", "") plt.savefig(args.output_directory / f"{gc}_power_time_overview") plt.close() - - -def generate(schedule, scenario, args): - """Generates all output files/ plots and saves them in the output directory. - - :param schedule: Driving schedule for the simulation. - :type schedule: simba.Schedule - :param scenario: Scenario for with to generate timeseries. - :type scenario: spice_ev.Scenario - :param args: Configuration arguments specified in config files contained in configs directory. - :type args: argparse.Namespace - :param extended_plots: Generates more plots. - :type extended_plots: bool - """ - - # generate if needed extended output plots - if extended_plots: - bus_type_distribution_consumption_rotation(args, schedule) - charge_type_proportion(args, schedule) - gc_power_time_overview(args, scenario) - - # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in SpiceEV - # re-route output paths - args.save_soc = args.results_directory / "vehicle_socs.csv" - args.save_results = args.results_directory / "info.json" - args.save_timeseries = args.results_directory / "ts.csv" - generate_reports(scenario, vars(args).copy()) - args.save_timeseries = None - args.save_results = None - args.save_soc = None - - # generate gc power overview - generate_gc_power_overview_timeseries(scenario, args) - - # generate gc overview - generate_gc_overview(schedule, scenario, args) - - # save plots as png and pdf - generate_plots(scenario, args) - - # calculate SOCs for each rotation - rotation_infos = [] - - negative_rotations = schedule.get_negative_rotations(scenario) - - interval = datetime.timedelta(minutes=args.interval) - sim_start_time = schedule.get_departure_of_first_trip() - # rotations might be empty, start_time is None - if sim_start_time: - sim_start_time -= datetime.timedelta(minutes=args.signal_time_dif) - else: - sim_start_time = datetime.datetime.fromtimestamp(0) - - incomplete_rotations = [] - rotation_socs = {} - for id, rotation in schedule.rotations.items(): - # get SOC timeseries for this rotation - vehicle_id = rotation.vehicle_id - - # get soc timeseries for current rotation - vehicle_soc = scenario.vehicle_socs[vehicle_id] - start_idx = (rotation.departure_time - sim_start_time) // interval - end_idx = start_idx + ((rotation.arrival_time - rotation.departure_time) // interval) - if end_idx > scenario.n_intervals: - # SpiceEV stopped before rotation was fully simulated - incomplete_rotations.append(id) - continue - rotation_soc_ts = vehicle_soc[start_idx:end_idx] - - # bus does not return before simulation end - # replace trailing None values with last numeric value - for i, soc in enumerate(reversed(rotation_soc_ts)): - if soc is not None: - break - last_known_idx = len(rotation_soc_ts) - 1 - i - rotation_soc_ts[last_known_idx + 1:] = i * [rotation_soc_ts[last_known_idx]] - - rotation_info = { - "rotation_id": id, - "start_time": rotation.departure_time.isoformat(), - "end_time": rotation.arrival_time.isoformat(), - "vehicle_type": rotation.vehicle_type, - "vehicle_id": rotation.vehicle_id, - "depot_name": rotation.departure_name, - "lines": ':'.join(rotation.lines), - "total_consumption_[kWh]": rotation.consumption, - "distance": rotation.distance, - "charging_type": rotation.charging_type, - "SOC_at_arrival": rotation_soc_ts[-1], - "Minimum_SOC": min(rotation_soc_ts), - "Negative_SOC": 1 if id in negative_rotations else 0 - } - rotation_infos.append(rotation_info) - - # save SOCs for each rotation - rotation_socs[id] = [None] * scenario.n_intervals - rotation_socs[id][start_idx:end_idx] = rotation_soc_ts - - if incomplete_rotations: - logging.warning( - "SpiceEV stopped before simulation of the these rotations were completed:\n" - f"{', '.join(incomplete_rotations)}\n" - "Omit parameter to simulate entire schedule.") - - if rotation_infos: - with open(args.results_directory / "rotation_socs.csv", "w", newline='') as f: - csv_writer = csv.writer(f) - # order rotations naturally - rotations = sorted(rotation_socs.keys(), key=lambda k: int(k)) - csv_writer.writerow(["time"] + rotations) - for i in range(scenario.n_intervals): - t = sim_start_time + i * scenario.interval - socs = [str(rotation_socs[k][i]) for k in rotations] - csv_writer.writerow([str(t)] + socs) - - with open(args.results_directory / "rotation_summary.csv", "w", newline='') as f: - csv_writer = csv.DictWriter(f, list(rotation_infos[0].keys())) - csv_writer.writeheader() - csv_writer.writerows(rotation_infos) - - # summary of used vehicle types and all costs - if args.cost_calculation: - with open(args.results_directory / "summary_vehicles_costs.csv", "w", newline='') as f: - csv_writer = csv.writer(f) - csv_writer.writerow(["parameter", "value", "unit"]) - for key, value in schedule.vehicle_type_counts.items(): - if value > 0: - csv_writer.writerow([key, value, "vehicles"]) - for key, value in scenario.costs.items(): - if "annual" in key: - csv_writer.writerow([key, round(value, 2), "€/year"]) - else: - csv_writer.writerow([key, round(value, 2), "€"]) - - logging.info(f"Plots and output files saved in {args.results_directory}") diff --git a/simba/util.py b/simba/util.py index 189fa7cc..5a6b404e 100644 --- a/simba/util.py +++ b/simba/util.py @@ -289,6 +289,8 @@ def get_args(): help='Remove rotations from schedule that violate assumptions. ') parser.add_argument('--show-plots', action='store_true', help='show plots for users to view in "report" mode') + parser.add_argument('--extended_output_plots', action='store_true', + help='show extended plots') parser.add_argument('--propagate-mode-errors', default=False, help='Re-raise errors instead of continuing during simulation modes') parser.add_argument('--create-scenario-file', help='Write scenario.json to file') From 53fe3e8d94abde86f10d7b23aabc65ad7201fc94 Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 21 May 2024 15:50:45 +0200 Subject: [PATCH 08/41] Fix cost_calculation in generate #94 --- simba/report.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/simba/report.py b/simba/report.py index f88e9fa1..2d6e00bc 100644 --- a/simba/report.py +++ b/simba/report.py @@ -256,20 +256,18 @@ def generate(schedule, scenario, args): # summary of used vehicle types and all costs if args.cost_calculation: - logging.info(f"Plots and output files saved in {args.output_directory}") - - file_path = args.results_directory / "summary_vehicles_costs.csv" - csv_report = None - try: - csv_report = scenario.costs.to_csv_lists() - except Exception as e: - logging.warning(f"Generating the cost calculation to {file_path} calculation failed " - f"due to {str(e)}") - if args.propagate_mode_errors: - raise - write_csv(csv_report, file_path, propagate_errors=args.propagate_mode_errors) - - logging.info(f"Plots and output files saved in {args.results_directory}") + file_path = args.results_directory / "summary_vehicles_costs.csv" + csv_report = None + try: + csv_report = scenario.costs.to_csv_lists() + except Exception as e: + logging.warning(f"Generating the cost calculation to {file_path} calculation failed " + f"due to {str(e)}") + if args.propagate_mode_errors: + raise + write_csv(csv_report, file_path, propagate_errors=args.propagate_mode_errors) + + logging.info(f"Plots and output files saved in {args.results_directory}") def generate_gc_power_overview_timeseries(scenario, args): From 1dde2180d215aea0145b48473479d17a54cdefa0 Mon Sep 17 00:00:00 2001 From: alexkens Date: Thu, 23 May 2024 13:56:59 +0200 Subject: [PATCH 09/41] Add active rotations to csv, add extended plots for active rotations #94 --- simba/report.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/simba/report.py b/simba/report.py index 2d6e00bc..5910eaf0 100644 --- a/simba/report.py +++ b/simba/report.py @@ -148,6 +148,7 @@ def generate(schedule, scenario, args): bus_type_distribution_consumption_rotation(args, schedule) charge_type_proportion(args, schedule) gc_power_time_overview(args, scenario) + active_rotations(args, scenario, schedule) # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in SpiceEV # re-route output paths @@ -246,6 +247,19 @@ def generate(schedule, scenario, args): t = sim_start_time + i * scenario.interval socs = [str(rotation_socs[k][i]) for k in rotations] data.append([str(t)] + socs) + + # add active rotations to rotation_socs.csv + number_of_active_vehicles = [len(scenario.components.vehicles)] * scenario.n_intervals + depot_stations = [station for station in scenario.components.grid_connectors + if schedule.stations[station]["type"] == "deps"] + for station_name in depot_stations: + station_ts = scenario.connChargeByTS[station_name] + for index, step in enumerate(station_ts): + number_of_active_vehicles[index] -= len(step) + data[0].append("# active_rotations") + for i in range(len(data)): + data[i+1].append(number_of_active_vehicles[i]) + write_csv(data, args.results_directory / "rotation_socs.csv", propagate_errors=args.propagate_mode_errors) @@ -511,6 +525,36 @@ def gc_power_time_overview(args, scenario): plt.close() +def active_rotations(args, scenario, schedule): + """Generate a plot where the number of active rotations is shown.""" + + number_of_vehicles = len(scenario.components.vehicles) + # ts = [[start_time + datetime.timedelta(minutes=step), 0] for step in range(scenario.n_intervals)] + ts = [ + scenario.start_time if i == 0 else + scenario.start_time + scenario.interval * i for i in range(scenario.n_intervals) + ] + number_of_active_vehicles = [number_of_vehicles] * scenario.n_intervals + + depot_stations = [station for station in scenario.components.grid_connectors + if schedule.stations[station]["type"] == "deps"] + + for station_name in depot_stations: + station_ts = scenario.connChargeByTS[station_name] + for index, step in enumerate(station_ts): + number_of_active_vehicles[index] -= len(step) + + plt.plot(ts, number_of_active_vehicles, label="total") + plt.legend() + plt.xlabel("Time") + plt.ylabel("Number of active Vehicles") + plt.title("Active Rotations") + plt.xticks(rotation=30) + + plt.savefig(args.output_directory / f"Active Rotations") + plt.close() + + def write_csv(data: Iterable, file_path, propagate_errors=False): """ Write iterable data to CSV file. From 04221de523d47ada98170b9318f10dc25dd5d7ee Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 28 May 2024 15:55:03 +0200 Subject: [PATCH 10/41] Fix flake8 and better naming #94 --- simba/report.py | 153 +++++++----------------------------------------- 1 file changed, 20 insertions(+), 133 deletions(-) diff --git a/simba/report.py b/simba/report.py index 5910eaf0..5fbf8378 100644 --- a/simba/report.py +++ b/simba/report.py @@ -128,8 +128,8 @@ def generate_plots(scenario, args): plt.show() # revert logging override logging.disable(logging.NOTSET) - - + + def generate(schedule, scenario, args): """ Generates all output files/ plots and saves them in the output directory. @@ -248,17 +248,17 @@ def generate(schedule, scenario, args): socs = [str(rotation_socs[k][i]) for k in rotations] data.append([str(t)] + socs) - # add active rotations to rotation_socs.csv - number_of_active_vehicles = [len(scenario.components.vehicles)] * scenario.n_intervals + # add active rotations column to rotation_socs.csv + active_vehicles = [len(scenario.components.vehicles)] * scenario.n_intervals depot_stations = [station for station in scenario.components.grid_connectors if schedule.stations[station]["type"] == "deps"] for station_name in depot_stations: station_ts = scenario.connChargeByTS[station_name] for index, step in enumerate(station_ts): - number_of_active_vehicles[index] -= len(step) + active_vehicles[index] -= len(step) data[0].append("# active_rotations") for i in range(len(data)): - data[i+1].append(number_of_active_vehicles[i]) + data[i].append(active_vehicles[i-1]) write_csv(data, args.results_directory / "rotation_socs.csv", propagate_errors=args.propagate_mode_errors) @@ -284,117 +284,6 @@ def generate(schedule, scenario, args): logging.info(f"Plots and output files saved in {args.results_directory}") -def generate_gc_power_overview_timeseries(scenario, args): - """Generate a csv timeseries with each grid connector's summed up charging station power - - :param scenario: Scenario for with to generate timeseries. - :type scenario: spice_ev.Scenario - :param args: Configuration arguments specified in config files contained in configs directory. - :type args: argparse.Namespace - """ - - gc_list = list(scenario.components.grid_connectors.keys()) - - if not gc_list: - return - - with open(args.output_directory / "gc_power_overview_timeseries.csv", "w", newline='') as f: - csv_writer = csv.writer(f) - csv_writer.writerow(["time"] + gc_list) - stations = [] - time_col = getattr(scenario, f"{gc_list[0]}_timeseries")["time"] - for i in range(len(time_col)): - time_col[i] = time_col[i].isoformat() - stations.append(time_col) - for gc in gc_list: - stations.append([-x for x in getattr(scenario, f"{gc}_timeseries")["grid supply [kW]"]]) - gc_power_overview = list(map(list, zip(*stations))) - csv_writer.writerows(gc_power_overview) - - -def generate_gc_overview(schedule, scenario, args): - """Generate a csv file with information regarding electrified stations. - - For each electrified station, the name, type, max. power, max. number of occupied - charging stations, sum of charged energy and use factors of least used stations is saved. - - :param schedule: Driving schedule for the simulation. - :type schedule: simba.Schedule - :param scenario: Scenario for with to generate timeseries. - :type scenario: spice_ev.Scenario - :param args: Configuration arguments specified in config files contained in configs directory. - :type args: argparse.Namespace - """ - - all_gc_list = list(schedule.stations.keys()) - used_gc_list = list(scenario.components.grid_connectors.keys()) - stations = getattr(schedule, "stations") - - with open(args.output_directory / "gc_overview.csv", "w", newline='') as f: - csv_writer = csv.writer(f) - csv_writer.writerow(["station_name", - "station_type", - "maximum_power", - "maximum Nr charging stations", - "sum of CS energy", - "use factor least CS", - "use factor 2nd least CS", - "use factor 3rd least CS"]) - for gc in all_gc_list: - if gc in used_gc_list: - ts = getattr(scenario, f"{gc}_timeseries") - max_gc_power = -min(ts["grid supply [kW]"]) - max_nr_cs = max(ts["# CS in use [-]"]) - sum_of_cs_energy = sum(ts["sum CS power [kW]"]) * args.interval/60 - - # use factors: to which percentage of time are the three least used CS in use - num_ts = scenario.n_intervals # number of timesteps - # three least used CS. Less if number of CS is lower. - least_used_num = min(3, max_nr_cs) - # count number of timesteps with this exact number of occupied CS - count_nr_cs = [ts["# CS in use [-]"].count(max_nr_cs - i) for i in range( - least_used_num)] - use_factors = [sum(count_nr_cs[:i + 1]) / num_ts for i in range( - least_used_num)] # relative occupancy with at least this number of occupied CS - use_factors = use_factors + [None] * (3 - least_used_num) # fill up line with None - - else: - max_gc_power = 0 - max_nr_cs = 0 - sum_of_cs_energy = 0 - use_factors = [None, None, None] - station_type = stations[gc]["type"] - csv_writer.writerow([gc, - station_type, - max_gc_power, - max_nr_cs, - sum_of_cs_energy, - *use_factors]) - - -def generate_plots(scenario, args): - """Save plots as png and pdf. - - :param scenario: Scenario to plot. - :type scenario: spice_ev.Scenario - :param args: Configuration. Uses output_directory and show_plots. - :type args: argparse.Namespace - """ - aggregate_global_results(scenario) - # disable DEBUG logging from matplotlib - logging.disable(logging.INFO) - with plt.ion(): # make plotting temporarily interactive, so plt.show does not block - plt.clf() - plot(scenario) - plt.gcf().set_size_inches(10, 10) - plt.savefig(args.output_directory / "run_overview.png") - plt.savefig(args.output_directory / "run_overview.pdf") - if args.show_plots: - plt.show() - # revert logging override - logging.disable(logging.NOTSET) - - def bus_type_distribution_consumption_rotation(args, schedule): """Plots the distribution of bus types in consumption brackets as a stacked bar chart. @@ -513,7 +402,10 @@ def gc_power_time_overview(args, scenario): ] plt.plot(ts, scenario.totalLoad[gc], label="total") plt.plot(ts, scenario.localGenerationPower[gc], label="feed_in") - ext_loads = [total - pv for total, pv in zip(scenario.totalLoad[gc], scenario.localGenerationPower[gc])] + ext_loads = [ + total - pv + for total, pv in zip(scenario.totalLoad[gc], scenario.localGenerationPower[gc]) + ] plt.plot(ts, ext_loads, label="ext_load") plt.legend() plt.xticks(rotation=30) @@ -527,31 +419,26 @@ def gc_power_time_overview(args, scenario): def active_rotations(args, scenario, schedule): """Generate a plot where the number of active rotations is shown.""" - - number_of_vehicles = len(scenario.components.vehicles) - # ts = [[start_time + datetime.timedelta(minutes=step), 0] for step in range(scenario.n_intervals)] ts = [ - scenario.start_time if i == 0 else - scenario.start_time + scenario.interval * i for i in range(scenario.n_intervals) + scenario.start_time if i == 0 else scenario.start_time + scenario.interval * i + for i in range(scenario.n_intervals) + ] + active_vehicles = [len(scenario.components.vehicles)] * scenario.n_intervals + depot_stations = [ + station for station in scenario.components.grid_connectors + if schedule.stations[station]["type"] == "deps" ] - number_of_active_vehicles = [number_of_vehicles] * scenario.n_intervals - - depot_stations = [station for station in scenario.components.grid_connectors - if schedule.stations[station]["type"] == "deps"] - for station_name in depot_stations: station_ts = scenario.connChargeByTS[station_name] for index, step in enumerate(station_ts): - number_of_active_vehicles[index] -= len(step) + active_vehicles[index] -= len(step) - plt.plot(ts, number_of_active_vehicles, label="total") + plt.plot(ts, active_vehicles, label="total") plt.legend() - plt.xlabel("Time") plt.ylabel("Number of active Vehicles") plt.title("Active Rotations") plt.xticks(rotation=30) - - plt.savefig(args.output_directory / f"Active Rotations") + plt.savefig(args.output_directory / "active_rotations") plt.close() From 210d6051704deff3873f0d3f242015ce496a69bf Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 28 May 2024 15:59:25 +0200 Subject: [PATCH 11/41] Fix flake8 #94 --- simba/report.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/simba/report.py b/simba/report.py index 5fbf8378..c13ef3fb 100644 --- a/simba/report.py +++ b/simba/report.py @@ -362,7 +362,7 @@ def charge_type_proportion(args, schedule): plt.close() -def gc_power_time_overview_example(args, schedule, scenario): +def gc_power_time_overview_example(args, scenario): gc_list = list(scenario.constants.grid_connectors.keys()) for gc in gc_list: @@ -418,7 +418,15 @@ def gc_power_time_overview(args, scenario): def active_rotations(args, scenario, schedule): - """Generate a plot where the number of active rotations is shown.""" + """Generate a plot where the number of active rotations is shown. + + :param args: Configuration arguments from cfg file. args.output_directory is used + :type args: argparse.Namespace + :param schedule: Driving schedule for the simulation. schedule.rotations are used + :type schedule: eBus-Toolbox.Schedule + :param scenario: Provides the data for the grid connectors over time. + :type scenario: spice_ev.Scenario + """ ts = [ scenario.start_time if i == 0 else scenario.start_time + scenario.interval * i for i in range(scenario.n_intervals) From 6db3b7fc0a2502431d2a51f8d5db557e3ad105ac Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 28 May 2024 16:13:20 +0200 Subject: [PATCH 12/41] Fix tests #94 --- tests/test_simulate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_simulate.py b/tests/test_simulate.py index 542cf206..d0ef6971 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -127,6 +127,7 @@ def test_mode_report(self, tmp_path): values["show_plots"] = False # tuned so that some rotations don't complete values["days"] = .33 + values["extended_output_plots"] = True with warnings.catch_warnings(): warnings.simplefilter("ignore") simulate(Namespace(**values)) @@ -142,6 +143,7 @@ def test_empty_report(self, tmp_path): "output_directory": tmp_path, "strategy": "distributed", "show_plots": False, + "extended_output_plots": False, }) with warnings.catch_warnings(): warnings.simplefilter("ignore") From a2195a46274801233abf9a675e4dea3d9b912123 Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 4 Jun 2024 13:28:09 +0200 Subject: [PATCH 13/41] Output in report folder, fix charge_type_proportion and gc_power_time_overview #94 --- simba/report.py | 123 ++++++++++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/simba/report.py b/simba/report.py index c13ef3fb..cd409cf3 100644 --- a/simba/report.py +++ b/simba/report.py @@ -5,7 +5,7 @@ from typing import Iterable import matplotlib.pyplot as plt -from spice_ev.report import aggregate_global_results, plot, generate_reports +from spice_ev.report import aggregate_global_results, plot, generate_reports, aggregate_timeseries def open_for_csv(filepath): @@ -131,7 +131,7 @@ def generate_plots(scenario, args): def generate(schedule, scenario, args): - """ Generates all output files/ plots and saves them in the output directory. + """ Generates all output files/ plots and saves them in the args.results_directory. :param schedule: Driving schedule for the simulation. :type schedule: simba.Schedule @@ -146,15 +146,15 @@ def generate(schedule, scenario, args): # generate if needed extended output plots if args.extended_output_plots: bus_type_distribution_consumption_rotation(args, schedule) - charge_type_proportion(args, schedule) + charge_type_proportion(args, scenario, schedule) gc_power_time_overview(args, scenario) active_rotations(args, scenario, schedule) # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in SpiceEV # re-route output paths - args.save_soc = args.output_directory / "vehicle_socs.csv" - args.save_results = args.output_directory / "info.json" - args.save_timeseries = args.output_directory / "ts.csv" + args.save_soc = args.results_directory / "vehicle_socs.csv" + args.save_results = args.results_directory / "info.json" + args.save_timeseries = args.results_directory / "ts.csv" generate_reports(scenario, vars(args).copy()) args.save_timeseries = None args.save_results = None @@ -287,7 +287,7 @@ def generate(schedule, scenario, args): def bus_type_distribution_consumption_rotation(args, schedule): """Plots the distribution of bus types in consumption brackets as a stacked bar chart. - :param args: Configuration arguments from cfg file. args.output_directory is used + :param args: Configuration arguments from cfg file. args.results_directory is used :type args: argparse.Namespace :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule @@ -325,40 +325,57 @@ def bus_type_distribution_consumption_rotation(args, schedule): ax.legend() fig.autofmt_xdate() ax.yaxis.grid(True) - plt.savefig(args.output_directory / "distribution_bustypes_consumption_rotations") + plt.savefig(args.results_directory / "distribution_bustypes_consumption_rotations") plt.close() -def charge_type_proportion(args, schedule): +def charge_type_proportion(args, scenario, schedule): """Plots the absolute number of rotations distributed by charging types on a bar chart. - :param args: Configuration arguments from cfg file. args.output_directory is used + :param args: Configuration arguments from cfg file. args.results_directory is used :type args: argparse.Namespace :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule """ # get plotting data - charging_types = {'oppb': 0, 'depb': 0, 'rest': 0} + charging_types = {'oppb': 0, 'oppb_neg': 0, 'depb': 0, 'depb_neg': 0} + negative_rotations = schedule.get_negative_rotations(scenario) for rot in schedule.rotations: if schedule.rotations[rot].charging_type == 'oppb': - charging_types['oppb'] += 1 + if rot in negative_rotations: + charging_types['oppb_neg'] += 1 + else: + charging_types['oppb'] += 1 elif schedule.rotations[rot].charging_type == 'depb': - charging_types['depb'] += 1 + if rot in negative_rotations: + charging_types['depb_neg'] += 1 + else: + charging_types['depb'] += 1 + # boxplot mit 2 Spalten, wo in jeder Spalte in negativ_soc und not_negative_soc eingordnet wird else: - charging_types['rest'] += 1 + print("Unknown charging type: ", schedule.rotations[rot].charging_type) # plot fig, ax = plt.subplots() - ax.bar( - [k for k in charging_types.keys()], - [v for v in charging_types.values()], - color=['#6495ED', '#66CDAA', 'grey'] + bars1 = ax.bar( + ["Gelegenheitslader", "Depotlader"], + [charging_types["oppb"], charging_types["depb"]], + color=["#6495ED", "#6495ED"], ) + bars2 = ax.bar( + ["Gelegenheitslader", "Depotlader"], + [charging_types["oppb_neg"], charging_types["depb_neg"]], + color=["#66CDAA", "#66CDAA"], + bottom=[charging_types["oppb"], charging_types["depb"]], + ) + ax.bar_label(bars1, labels=[f"{charging_types['oppb']} oppb", f"{charging_types['depb']} depb"]) + ax.bar_label(bars2, labels=[f"{charging_types['oppb_neg']} oppb_neg", f"{charging_types['depb_neg']} depb_neg"]) + ax.set_xlabel("Ladetyp") ax.set_ylabel("Umläufe") - ax.set_title("Verteilung von Gelegenheitslader, Depotlader und nicht elektrifizierbaren") - ax.bar_label(ax.containers[0], [v for v in [v for v in charging_types.values()]]) + ax.set_title("Verteilung von Gelegenheitslader, Depotlader") + ax.yaxis.grid(True) - plt.savefig(args.output_directory / "charge_type_proportion") + plt.savefig(args.results_directory / "charge_type_proportion") plt.close() @@ -382,45 +399,59 @@ def gc_power_time_overview_example(args, scenario): plt.legend() plt.xticks(rotation=45) - plt.savefig(args.output_directory / f"{gc}_power_time_overview") + plt.savefig(args.results_directory / f"{gc}_power_time_overview") plt.clf() def gc_power_time_overview(args, scenario): """Plots the different loads (total, feedin, external) of all grid connectors. - :param args: Configuration arguments from cfg file. args.output_directory is used + :param args: Configuration arguments from cfg file. args.results_directory is used :type args: argparse.Namespace :param scenario: Provides the data for the grid connectors over time. :type scenario: spice_ev.Scenario """ gc_list = list(scenario.components.grid_connectors.keys()) - for gc in gc_list: - ts = [ - scenario.start_time if i == 0 else - scenario.start_time+scenario.interval*i for i in range(scenario.n_intervals) - ] - plt.plot(ts, scenario.totalLoad[gc], label="total") - plt.plot(ts, scenario.localGenerationPower[gc], label="feed_in") - ext_loads = [ - total - pv - for total, pv in zip(scenario.totalLoad[gc], scenario.localGenerationPower[gc]) - ] - plt.plot(ts, ext_loads, label="ext_load") - plt.legend() - plt.xticks(rotation=30) - plt.ylabel("Power in kW") - plt.title(f"Power: {gc}") - gc = gc.replace("/", "").replace(".", "") - plt.savefig(args.output_directory / f"{gc}_power_time_overview") - plt.close() + for gc in gc_list: + fig, ax = plt.subplots() + + agg_ts = aggregate_timeseries(scenario, gc) + headers = ["grid supply [kW]", "fixed load [kW]", "local generation [kW]", "sum CS power [kW]", + "battery power [kW]", "bat. stored energy [kWh]"] + time_index = agg_ts["header"].index("time") + time_values = [row[time_index] for row in agg_ts["timeseries"]] + + for header in headers: + try: + header_index = agg_ts["header"].index(header) + header_values = [row[header_index] for row in agg_ts["timeseries"]] + + if header == "bat. stored energy [kWh]": + ax2 = ax.twinx() # Instantiate a second Axes that shares the same x-axis + ax2.set_ylabel("Power in kWh") # We already handled the x-label with ax1 + ax2.plot(time_values, header_values, label=header) + ax2.legend() + ax2.tick_params(axis='x', rotation=30) + else: + ax.plot(time_values, header_values, label=header) # Use ax instead of plt + except ValueError: + continue + + ax.legend() + ax.tick_params(axis='x', rotation=30) + ax.set_ylabel("Power in kW") + ax.set_title(f"Power: {gc}") + + gc_cleaned = gc.replace("/", "").replace(".", "") + plt.savefig(args.results_directory / f"{gc_cleaned}_power_time_overview.png") + plt.close(fig) def active_rotations(args, scenario, schedule): """Generate a plot where the number of active rotations is shown. - :param args: Configuration arguments from cfg file. args.output_directory is used + :param args: Configuration arguments from cfg file. args.results_directory is used :type args: argparse.Namespace :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule @@ -441,12 +472,12 @@ def active_rotations(args, scenario, schedule): for index, step in enumerate(station_ts): active_vehicles[index] -= len(step) - plt.plot(ts, active_vehicles, label="total") - plt.legend() + plt.plot(ts, active_vehicles) plt.ylabel("Number of active Vehicles") plt.title("Active Rotations") plt.xticks(rotation=30) - plt.savefig(args.output_directory / "active_rotations") + plt.grid(axis="y") + plt.savefig(args.results_directory / "active_rotations") plt.close() From 2d991835316a472999f5281d713eb6697ae3a1c7 Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 4 Jun 2024 13:30:29 +0200 Subject: [PATCH 14/41] Flake8 #94 --- simba/report.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/simba/report.py b/simba/report.py index cd409cf3..dea0ea70 100644 --- a/simba/report.py +++ b/simba/report.py @@ -351,7 +351,6 @@ def charge_type_proportion(args, scenario, schedule): charging_types['depb_neg'] += 1 else: charging_types['depb'] += 1 - # boxplot mit 2 Spalten, wo in jeder Spalte in negativ_soc und not_negative_soc eingordnet wird else: print("Unknown charging type: ", schedule.rotations[rot].charging_type) # plot @@ -367,8 +366,14 @@ def charge_type_proportion(args, scenario, schedule): color=["#66CDAA", "#66CDAA"], bottom=[charging_types["oppb"], charging_types["depb"]], ) - ax.bar_label(bars1, labels=[f"{charging_types['oppb']} oppb", f"{charging_types['depb']} depb"]) - ax.bar_label(bars2, labels=[f"{charging_types['oppb_neg']} oppb_neg", f"{charging_types['depb_neg']} depb_neg"]) + ax.bar_label(bars1, labels=[ + f"{charging_types['oppb']} oppb", + f"{charging_types['depb']} depb", + ]) + ax.bar_label(bars2, labels=[ + f"{charging_types['oppb_neg']} oppb_neg", + f"{charging_types['depb_neg']} depb_neg", + ]) ax.set_xlabel("Ladetyp") ax.set_ylabel("Umläufe") @@ -417,8 +422,14 @@ def gc_power_time_overview(args, scenario): fig, ax = plt.subplots() agg_ts = aggregate_timeseries(scenario, gc) - headers = ["grid supply [kW]", "fixed load [kW]", "local generation [kW]", "sum CS power [kW]", - "battery power [kW]", "bat. stored energy [kWh]"] + headers = [ + "grid supply [kW]", + "fixed load [kW]", + "local generation [kW]", + "sum CS power [kW]", + "battery power [kW]", + "bat. stored energy [kWh]", + ] time_index = agg_ts["header"].index("time") time_values = [row[time_index] for row in agg_ts["timeseries"]] From 107a2ea00b761f111e5bb1bd15b361b3d130fed1 Mon Sep 17 00:00:00 2001 From: alexkens Date: Tue, 4 Jun 2024 13:33:49 +0200 Subject: [PATCH 15/41] Flake8 #94 --- simba/report.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simba/report.py b/simba/report.py index dea0ea70..a85144d9 100644 --- a/simba/report.py +++ b/simba/report.py @@ -334,6 +334,8 @@ def charge_type_proportion(args, scenario, schedule): :param args: Configuration arguments from cfg file. args.results_directory is used :type args: argparse.Namespace + :param scenario: Scenario for with to generate timeseries. + :type scenario: spice_ev.Scenario :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule """ From 8dc0452fb017a436ceb442f5b838b90839c42a6e Mon Sep 17 00:00:00 2001 From: alexkens Date: Fri, 7 Jun 2024 16:01:03 +0200 Subject: [PATCH 16/41] Add extended plot folder, add route rotation plot, replace parameter args with path #94 --- simba/report.py | 106 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/simba/report.py b/simba/report.py index a85144d9..f74f2628 100644 --- a/simba/report.py +++ b/simba/report.py @@ -143,12 +143,16 @@ def generate(schedule, scenario, args): args.propagate_mode_errors=True """ - # generate if needed extended output plots if args.extended_output_plots: - bus_type_distribution_consumption_rotation(args, schedule) - charge_type_proportion(args, scenario, schedule) - gc_power_time_overview(args, scenario) - active_rotations(args, scenario, schedule) + # create directory for extended plots + extended_plots_path = args.results_directory.joinpath("extended_plots") + extended_plots_path.mkdir(parents=True, exist_ok=True) + + bus_type_distribution_consumption_rotation(extended_plots_path, schedule) + bus_type_distribution_route_rotation(extended_plots_path, schedule) + charge_type_proportion(extended_plots_path, scenario, schedule) + gc_power_time_overview(extended_plots_path, scenario) + active_rotations(extended_plots_path, scenario, schedule) # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in SpiceEV # re-route output paths @@ -284,11 +288,51 @@ def generate(schedule, scenario, args): logging.info(f"Plots and output files saved in {args.results_directory}") -def bus_type_distribution_consumption_rotation(args, schedule): +def bus_type_distribution_route_rotation(extended_plots_path, schedule): """Plots the distribution of bus types in consumption brackets as a stacked bar chart. - :param args: Configuration arguments from cfg file. args.results_directory is used - :type args: argparse.Namespace + :param extended_plots_path: Path argument which combines args.results_directory + and string 'extended_plots' + :type extended_plots_path: PosixPath + :param schedule: Driving schedule for the simulation. schedule.rotations are used + :type schedule: eBus-Toolbox.Schedule + """ + + step = 50 + max_route = int(max([schedule.rotations[rot].distance / 1000. for rot in schedule.rotations])) + bin_number = max_route // step + 1 + labels = [f"{i - step} - {i}" for i in range(step, bin_number * step + step, step) if i > 0] + bins = {v_types: [0 for _ in range(bin_number)] for v_types in schedule.vehicle_types} + + # fill bins with rotations + for rot in schedule.rotations: + for v_type in schedule.vehicle_types: + if schedule.rotations[rot].vehicle_type == v_type: + position = int((schedule.rotations[rot].distance / 1000.) // step) + bins[v_type][position] += 1 + # plot + fig, ax = plt.subplots() + bar_bottom = [0 for _ in range(bin_number)] + for v_type in schedule.vehicle_types: + ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) + for i in range(bin_number): + bar_bottom[i] += bins[v_type][i] + ax.set_xlabel('Strecke in km') + ax.set_ylabel('Anzahl der Umläufe') + ax.set_title('Verteilung der Bustypen über die Streckenlänge der Umläufe') + ax.legend() + fig.autofmt_xdate() + ax.yaxis.grid(True) + plt.savefig(extended_plots_path / "distribution_bustypes_route_rotations") + plt.close() + + +def bus_type_distribution_consumption_rotation(extended_plots_path, schedule): + """Plots the distribution of bus types in consumption brackets as a stacked bar chart. + + :param extended_plots_path: Path argument which combines args.results_directory + and string 'extended_plots' + :type extended_plots_path: PosixPath :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule """ @@ -321,19 +365,20 @@ def bus_type_distribution_consumption_rotation(args, schedule): bar_bottom[i] += bins[v_type][i] ax.set_xlabel('Energieverbrauch in kWh') ax.set_ylabel('Anzahl der Umläufe') - ax.set_title('Verteilung der Bustypen über den Energieverbrauch und den Umläufen') + ax.set_title('Verteilung der Bustypen über den Energieverbrauch der Umläufe') ax.legend() fig.autofmt_xdate() ax.yaxis.grid(True) - plt.savefig(args.results_directory / "distribution_bustypes_consumption_rotations") + plt.savefig(extended_plots_path / "distribution_bustypes_consumption_rotations") plt.close() -def charge_type_proportion(args, scenario, schedule): +def charge_type_proportion(extended_plots_path, scenario, schedule): """Plots the absolute number of rotations distributed by charging types on a bar chart. - :param args: Configuration arguments from cfg file. args.results_directory is used - :type args: argparse.Namespace + :param extended_plots_path: Path argument which combines args.results_directory + and string 'extended_plots' + :type extended_plots_path: PosixPath :param scenario: Scenario for with to generate timeseries. :type scenario: spice_ev.Scenario :param schedule: Driving schedule for the simulation. schedule.rotations are used @@ -382,7 +427,7 @@ def charge_type_proportion(args, scenario, schedule): ax.set_title("Verteilung von Gelegenheitslader, Depotlader") ax.yaxis.grid(True) - plt.savefig(args.results_directory / "charge_type_proportion") + plt.savefig(extended_plots_path / "charge_type_proportion") plt.close() @@ -410,11 +455,12 @@ def gc_power_time_overview_example(args, scenario): plt.clf() -def gc_power_time_overview(args, scenario): +def gc_power_time_overview(extended_plots_path, scenario): """Plots the different loads (total, feedin, external) of all grid connectors. - :param args: Configuration arguments from cfg file. args.results_directory is used - :type args: argparse.Namespace + :param extended_plots_path: Path argument which combines args.results_directory + and string 'extended_plots' + :type extended_plots_path: PosixPath :param scenario: Provides the data for the grid connectors over time. :type scenario: spice_ev.Scenario """ @@ -432,18 +478,24 @@ def gc_power_time_overview(args, scenario): "battery power [kW]", "bat. stored energy [kWh]", ] + # todos + # beide y achsen gleich skalieren, gleiche Tickanzahl, 0 Elemente auf gleicher Höhe + # label von rechter y-Achse ist nicht zu sehen + time_index = agg_ts["header"].index("time") time_values = [row[time_index] for row in agg_ts["timeseries"]] - for header in headers: + for index, header in enumerate(headers): try: header_index = agg_ts["header"].index(header) header_values = [row[header_index] for row in agg_ts["timeseries"]] if header == "bat. stored energy [kWh]": - ax2 = ax.twinx() # Instantiate a second Axes that shares the same x-axis - ax2.set_ylabel("Power in kWh") # We already handled the x-label with ax1 - ax2.plot(time_values, header_values, label=header) + ax2 = ax.twinx() + ax2.set_ylabel("Power in kWh") + next_color = plt.rcParams['axes.prop_cycle'].by_key()["color"][index] + ax2.plot(time_values, header_values, label=header, c=next_color, + linestyle="dashed") ax2.legend() ax2.tick_params(axis='x', rotation=30) else: @@ -455,17 +507,19 @@ def gc_power_time_overview(args, scenario): ax.tick_params(axis='x', rotation=30) ax.set_ylabel("Power in kW") ax.set_title(f"Power: {gc}") + ax.grid(color='gray', linestyle='-') gc_cleaned = gc.replace("/", "").replace(".", "") - plt.savefig(args.results_directory / f"{gc_cleaned}_power_time_overview.png") + plt.savefig(extended_plots_path / f"{gc_cleaned}_power_time_overview.png") plt.close(fig) -def active_rotations(args, scenario, schedule): +def active_rotations(extended_plots_path, scenario, schedule): """Generate a plot where the number of active rotations is shown. - :param args: Configuration arguments from cfg file. args.results_directory is used - :type args: argparse.Namespace + :param extended_plots_path: Path argument which combines args.results_directory + and string 'extended_plots' + :type extended_plots_path: PosixPath :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule :param scenario: Provides the data for the grid connectors over time. @@ -490,7 +544,7 @@ def active_rotations(args, scenario, schedule): plt.title("Active Rotations") plt.xticks(rotation=30) plt.grid(axis="y") - plt.savefig(args.results_directory / "active_rotations") + plt.savefig(extended_plots_path / "active_rotations") plt.close() From d78c655aac036bc06005115ecf21644fe572b995 Mon Sep 17 00:00:00 2001 From: alexkens Date: Wed, 12 Jun 2024 13:54:39 +0200 Subject: [PATCH 17/41] Fix second y-axis #94 --- simba/report.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/simba/report.py b/simba/report.py index f74f2628..29654b6d 100644 --- a/simba/report.py +++ b/simba/report.py @@ -478,9 +478,6 @@ def gc_power_time_overview(extended_plots_path, scenario): "battery power [kW]", "bat. stored energy [kWh]", ] - # todos - # beide y achsen gleich skalieren, gleiche Tickanzahl, 0 Elemente auf gleicher Höhe - # label von rechter y-Achse ist nicht zu sehen time_index = agg_ts["header"].index("time") time_values = [row[time_index] for row in agg_ts["timeseries"]] @@ -495,13 +492,18 @@ def gc_power_time_overview(extended_plots_path, scenario): ax2.set_ylabel("Power in kWh") next_color = plt.rcParams['axes.prop_cycle'].by_key()["color"][index] ax2.plot(time_values, header_values, label=header, c=next_color, - linestyle="dashed") + linestyle="dashdot") ax2.legend() ax2.tick_params(axis='x', rotation=30) + fig.set_size_inches(8, 4.8) else: - ax.plot(time_values, header_values, label=header) # Use ax instead of plt + ax.plot(time_values, header_values, label=header) except ValueError: continue + try: + ax2.set_ylim(ax.get_ylim()) + except NameError: + continue ax.legend() ax.tick_params(axis='x', rotation=30) From c4ce0378a8efb3a5da2e376c6a87240e7941f2ce Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 7 Aug 2024 10:46:50 +0200 Subject: [PATCH 18/41] add missing input files to output --- simba/__main__.py | 20 ++++++++------------ simba/consumption.py | 15 ++++++++++++--- simba/schedule.py | 4 ++++ simba/simulate.py | 5 +---- simba/util.py | 30 ++++++++++++++++++++++++++++++ tests/helpers.py | 10 +++++++--- tests/test_schedule.py | 28 ++++++++++------------------ tests/test_station_optimization.py | 12 +++++------- 8 files changed, 77 insertions(+), 47 deletions(-) diff --git a/simba/__main__.py b/simba/__main__.py index a2db7b98..f3cb8aa4 100644 --- a/simba/__main__.py +++ b/simba/__main__.py @@ -1,7 +1,6 @@ from datetime import datetime import logging from pathlib import Path -import shutil from simba import simulate, util @@ -21,18 +20,15 @@ except NotADirectoryError: # can't create new directory (may be write protected): no output args.output_directory = None - if args.output_directory is not None: - # copy input files to output to ensure reproducibility - copy_list = [args.config, args.electrified_stations, args.vehicle_types] - if "station_optimization" in args.mode: - copy_list.append(args.optimizer_config) - - # only copy cost params if they exist - if args.cost_parameters_file is not None: - copy_list.append(args.cost_parameters_file) - for c_file in map(Path, copy_list): - shutil.copy(c_file, args.output_directory_input / c_file.name) + # copy basic input files to output to ensure reproducibility + if args.output_directory is not None: + copy_list = [ + args.config, args.input_schedule, + args.electrified_stations, args.vehicle_types, + args.cost_parameters_file] + for input_file in copy_list: + util.save_input_file(input_file, args) util.save_version(args.output_directory_input / "program_version.txt") util.setup_logging(args, time_str) diff --git a/simba/consumption.py b/simba/consumption.py index fb00143a..99ecd526 100644 --- a/simba/consumption.py +++ b/simba/consumption.py @@ -4,11 +4,11 @@ class Consumption: - def __init__(self, vehicle_types, **kwargs) -> None: + def __init__(self, vehicle_types, args) -> None: # load temperature of the day, now dummy winter day self.temperatures_by_hour = {} - temperature_file_path = kwargs.get("outside_temperatures", None) + temperature_file_path = vars(args).get("outside_temperature_over_day_path", None) # parsing the Temperature to a dict if temperature_file_path is not None: with open(temperature_file_path, encoding='utf-8') as f: @@ -16,8 +16,9 @@ def __init__(self, vehicle_types, **kwargs) -> None: reader = csv.DictReader(f, delimiter=delim) for row in reader: self.temperatures_by_hour.update({int(row['hour']): float(row['temperature'])}) + util.save_input_file(temperature_file_path, args) - lol_file_path = kwargs.get("level_of_loading_over_day", None) + lol_file_path = vars(args).get("level_of_loading_over_day_path", None) # parsing the level of loading to a dict if lol_file_path is not None: with open(lol_file_path, encoding='utf-8') as f: @@ -26,6 +27,14 @@ def __init__(self, vehicle_types, **kwargs) -> None: self.lol_by_hour = {} for row in reader: self.lol_by_hour.update({int(row['hour']): float(row['level_of_loading'])}) + util.save_input_file(lol_file_path, args) + + # save mileage files + for vt in vehicle_types.values(): + for vt_info in vt.values(): + mileage = vt_info.get("mileage") + if mileage is not None and not isinstance(mileage, (int, float)): + util.save_input_file(mileage, args) self.consumption_files = {} self.vehicle_types = vehicle_types diff --git a/simba/schedule.py b/simba/schedule.py index 98550307..ca77f5cb 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -672,6 +672,7 @@ def generate_scenario(self, args): if time_windows_path.exists(): with time_windows_path.open('r', encoding='utf-8') as f: time_windows = util.uncomment_json_file(f) + util.save_input_file(args.time_windows, args) # convert time window strings to date/times for grid_operator, grid_operator_seasons in time_windows.items(): for season, info in grid_operator_seasons.items(): @@ -797,6 +798,7 @@ def generate_scenario(self, args): if local_generation: local_generation = update_csv_file_info(local_generation, gc_name) events["local_generation"][gc_name + " feed-in"] = local_generation + util.save_input_file(local_generation["csv_file"], args) # add PV component photovoltaics[gc_name] = { "parent": gc_name, @@ -808,6 +810,7 @@ def generate_scenario(self, args): if fixed_load: fixed_load = update_csv_file_info(fixed_load, gc_name) events["fixed_load"][gc_name + " ext. load"] = fixed_load + util.save_input_file(fixed_load["csv_file"], args) # temporary lowering of grid connector max power during peak load windows if time_windows is not None: @@ -879,6 +882,7 @@ def generate_scenario(self, args): else: # read prices from CSV, convert to events prices = get_price_list_from_csv(price_csv) + util.save_input_file(price_csv["csv_file"], args) events["grid_operator_signals"] += generate_event_list_from_prices( prices, gc_name, start_simulation, stop_simulation, price_csv.get('start_time'), price_csv.get('step_duration_s')) diff --git a/simba/simulate.py b/simba/simulate.py index 67092284..670d563b 100644 --- a/simba/simulate.py +++ b/simba/simulate.py @@ -64,10 +64,7 @@ def pre_simulation(args): "does not exist. Exiting...") # setup consumption calculator that can be accessed by all trips - Trip.consumption = Consumption( - vehicle_types, - outside_temperatures=args.outside_temperature_over_day_path, - level_of_loading_over_day=args.level_of_loading_over_day_path) + Trip.consumption = Consumption(vehicle_types, args) # generate schedule from csv schedule = Schedule.from_csv(args.input_schedule, vehicle_types, stations, **vars(args)) diff --git a/simba/util.py b/simba/util.py index 2c2ffe61..9eed3519 100644 --- a/simba/util.py +++ b/simba/util.py @@ -1,6 +1,8 @@ import argparse import json import logging +from pathlib import Path +import shutil import subprocess from spice_ev.strategy import STRATEGIES @@ -16,6 +18,34 @@ def save_version(file_path): f.write("Git Hash SimBA:" + get_git_revision_hash()) +def save_input_file(file_path, args): + """ Copy given file to output folder, to ensure reproducibility. + + *file_path* must exist and *output_directory_input* must be set in *args*. + If either condition is not met or the file has already been copied, nothing is done. + + :param file_path: source file + :type file_path: string or Path + :param args: general info, output_directory_input is required + :type args: Namespace + """ + if file_path is None: + return + output_directory_input = vars(args).get("output_directory_input", None) + if output_directory_input is None: + # input directory was not created + return + source_path = Path(file_path) + target_path = output_directory_input / source_path.name + if not source_path.exists(): + # original file missing + return + if target_path.exists(): + # already saved + return + shutil.copy(source_path, target_path) + + def get_buffer_time(trip, default=0): """ Get buffer time at arrival station of a trip. diff --git a/tests/helpers.py b/tests/helpers.py index 8ce40239..86ca729e 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,6 @@ """ Reusable functions that support tests """ +from argparse import Namespace from simba import schedule, trip, consumption, util @@ -12,9 +13,12 @@ def generate_basic_schedule(): vehicle_types = util.uncomment_json_file(f) with open(station_path, 'r', encoding='utf-8') as f: stations = util.uncomment_json_file(f) - trip.Trip.consumption = consumption.Consumption(vehicle_types, - outside_temperatures=temperature_path, - level_of_loading_over_day=lol_path) + trip.Trip.consumption = consumption.Consumption( + vehicle_types, Namespace( + outside_temperature_over_day_path=temperature_path, + level_of_loading_over_day_path=lol_path + ) + ) mandatory_args = { "min_recharge_deps_oppb": 0, diff --git a/tests/test_schedule.py b/tests/test_schedule.py index 62af0cdc..73a84022 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -26,6 +26,10 @@ class BasicSchedule: temperature_path = example_root / 'default_temp_winter.csv' lol_path = example_root / 'default_level_of_loading_over_day.csv' + test_args = Namespace( + outside_temperature_over_day_path=temperature_path, + level_of_loading_over_day_path=lol_path + ) with open(example_root / "electrified_stations.json", "r", encoding='utf-8') as file: electrified_stations = util.uncomment_json_file(file) @@ -71,9 +75,7 @@ def test_station_data_reading(self): case the data was problematic, e.g. not numeric or not existent""" path_to_trips = example_root / "trips_example.csv" - trip.Trip.consumption = consumption.Consumption( - self.vehicle_types, outside_temperatures=self.temperature_path, - level_of_loading_over_day=self.lol_path) + trip.Trip.consumption = consumption.Consumption(self.vehicle_types, self.test_args) path_to_all_station_data = example_root / "all_stations.csv" generated_schedule = schedule.Schedule.from_csv( @@ -105,9 +107,7 @@ def test_assign_vehicles_fixed_recharge(self): """ Test if assigning vehicles works as intended using the fixed_recharge strategy """ - trip.Trip.consumption = consumption.Consumption(self.vehicle_types, - outside_temperatures=self.temperature_path, - level_of_loading_over_day=self.lol_path) + trip.Trip.consumption = consumption.Consumption(self.vehicle_types, self.test_args) path_to_trips = file_root / "trips_assign_vehicles_extended.csv" generated_schedule = schedule.Schedule.from_csv( @@ -152,9 +152,7 @@ def test_assign_vehicles_adaptive(self): """ Test if assigning vehicles works as intended using the adaptive strategy """ - trip.Trip.consumption = consumption.Consumption(self.vehicle_types, - outside_temperatures=self.temperature_path, - level_of_loading_over_day=self.lol_path) + trip.Trip.consumption = consumption.Consumption(self.vehicle_types, self.test_args) path_to_trips = file_root / "trips_assign_vehicles_extended.csv" generated_schedule = schedule.Schedule.from_csv( @@ -205,9 +203,7 @@ def test_calculate_consumption(self): """ # Changing self.vehicle_types can propagate to other tests vehicle_types = deepcopy(self.vehicle_types) - trip.Trip.consumption = consumption.Consumption( - vehicle_types, outside_temperatures=self.temperature_path, - level_of_loading_over_day=self.lol_path) + trip.Trip.consumption = consumption.Consumption(vehicle_types, self.test_args) path_to_trips = file_root / "trips_assign_vehicles.csv" generated_schedule = schedule.Schedule.from_csv( @@ -230,9 +226,7 @@ def test_get_common_stations(self): on the second day. rotation 1 should not share any stations with other rotations and 2 and 3 are almost simultaneous. """ - trip.Trip.consumption = consumption.Consumption( - self.vehicle_types, outside_temperatures=self.temperature_path, - level_of_loading_over_day=self.lol_path) + trip.Trip.consumption = consumption.Consumption(self.vehicle_types, self.test_args) path_to_trips = file_root / "trips_assign_vehicles.csv" generated_schedule = schedule.Schedule.from_csv( @@ -335,9 +329,7 @@ def test_scenario_with_feed_in(self): args.days = None args.seed = 5 - trip.Trip.consumption = consumption.Consumption( - self.vehicle_types, outside_temperatures=self.temperature_path, - level_of_loading_over_day=self.lol_path) + trip.Trip.consumption = consumption.Consumption(self.vehicle_types, self.test_args) path_to_all_station_data = example_root / "all_stations.csv" generated_schedule = schedule.Schedule.from_csv( diff --git a/tests/test_station_optimization.py b/tests/test_station_optimization.py index 638bfe55..96909702 100644 --- a/tests/test_station_optimization.py +++ b/tests/test_station_optimization.py @@ -1,6 +1,7 @@ -import logging +from argparse import Namespace from copy import copy, deepcopy import json +import logging from pathlib import Path import pytest import random @@ -117,14 +118,11 @@ def basic_run(self, trips_file_name="trips.csv"): sys.argv = ["foo", "--config", str(self.tmp_path / "simba.cfg")] args = util.get_args() args.input_schedule = path_to_trips - Trip.consumption = Consumption(self.vehicle_types, - outside_temperatures=None, - level_of_loading_over_day=None) + Trip.consumption = Consumption(self.vehicle_types, Namespace()) args2 = copy(args) del args2.vehicle_types - generated_schedule = Schedule.from_csv(path_to_trips, self.vehicle_types, - self.electrified_stations, - **vars(args2)) + generated_schedule = Schedule.from_csv( + path_to_trips, self.vehicle_types, self.electrified_stations, **vars(args2)) scen = generated_schedule.run(args) # optimization depends on vehicle_socs, therefore they need to be generated From eb542f05cbebbcb5443b307701863b4c7fe9c1ef Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 7 Aug 2024 11:52:11 +0200 Subject: [PATCH 19/41] add rot_filter to input_data, add tests --- simba/schedule.py | 1 + tests/test_example.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/simba/schedule.py b/simba/schedule.py index ca77f5cb..9488e408 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -621,6 +621,7 @@ def rotation_filter(self, args, rf_list=[]): warnings.warn(f"Path to rotation filter {args.rotation_filter} is invalid.") # no file, no change return + util.save_input_file(args.rotation_filter, args) # filter out rotations in self.rotations if args.rotation_filter_variable == "exclude": self.rotations = {k: v for k, v in self.rotations.items() if k not in rf_list} diff --git a/tests/test_example.py b/tests/test_example.py index 36aca004..8764f63a 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -25,3 +25,28 @@ def test_example_cfg(self, tmp_path): assert subprocess.call([ "python", "-m", "simba", "--config", dst ]) == 0 + + # make sure all required files have been copied to output folder + expected = [ + 'simba.cfg', + 'program_version.txt', + 'trips_example.csv', + 'electrified_stations.json' + 'vehicle_types.json', + + 'cost_params.json', + 'default_level_of_loading_over_day.csv', + 'default_temp_winter.csv', + 'energy_consumption_example.csv', + 'example_pv_feedin.csv', + 'example_external_load.csv', + 'price_timeseries.csv', + 'time_windows.json', + ] + input_dir = next(tmp_path.glob('*/input_data')) + missing = list() + for file_name in expected: + if not (input_dir / file_name).exists(): + missing.append(file_name) + if missing: + raise Exception("Missing input files in output directory: " + ', '.join(missing)) From de13dd7eec466d2766dc1fe966e551abed18231f Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 7 Aug 2024 12:45:48 +0200 Subject: [PATCH 20/41] fix example test --- tests/test_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_example.py b/tests/test_example.py index 8764f63a..d0ea2636 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -31,7 +31,7 @@ def test_example_cfg(self, tmp_path): 'simba.cfg', 'program_version.txt', 'trips_example.csv', - 'electrified_stations.json' + 'electrified_stations.json', 'vehicle_types.json', 'cost_params.json', From c567fd0f34c717b53a63d6612d7f0f4450daeae6 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Tue, 17 Sep 2024 16:07:26 +0200 Subject: [PATCH 21/41] rework extended output plots --- data/examples/simba.cfg | 3 +- simba/report.py | 424 ++++++++++++++++++++-------------------- 2 files changed, 210 insertions(+), 217 deletions(-) diff --git a/data/examples/simba.cfg b/data/examples/simba.cfg index 069fea5e..f62b5dee 100644 --- a/data/examples/simba.cfg +++ b/data/examples/simba.cfg @@ -54,7 +54,8 @@ check_rotation_consistency = false skip_inconsistent_rotations = false # Show plots for users to view, only used in mode report (default: false) show_plots = false -extended_output_plots = true +# generate special plots in report mode (default: false) +extended_output_plots = false # Rotation filter variable, options: # "include": include only the rotations from file 'rotation_filter' # "exclude": exclude the rotations from file 'rotation_filter' from the schedule diff --git a/simba/report.py b/simba/report.py index 85accbd9..a44c7d17 100644 --- a/simba/report.py +++ b/simba/report.py @@ -2,11 +2,13 @@ import csv import datetime import logging +from math import ceil import re from typing import Iterable import matplotlib.pyplot as plt from spice_ev.report import aggregate_global_results, plot, generate_reports, aggregate_timeseries +from spice_ev.util import sanitize def open_for_csv(file_path): @@ -153,12 +155,16 @@ def generate_trips_timeseries_data(schedule): return data -def generate_plots(scenario, args): +def generate_plots(schedule, scenario, args): """ Save plots as png and pdf. + Optionally create extended output plots as well. + + :param schedule: Driving schedule for the simulation, used in extended plots. + :type schedule: simba.Schedule :param scenario: Scenario to plot. :type scenario: spice_ev.Scenario - :param args: Configuration. Uses results_directory and show_plots. + :param args: Configuration. Uses results_directory, show_plots and extended_output_plots. :type args: argparse.Namespace """ aggregate_global_results(scenario) @@ -178,12 +184,25 @@ def generate_plots(scenario, args): plt.savefig(file_path_pdf) if args.show_plots: plt.show() + + if args.extended_output_plots: + plt.clf() + # create directory for extended plots + extended_plots_path = args.results_directory.joinpath("extended_plots") + extended_plots_path.mkdir(parents=True, exist_ok=True) + + 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) + plot_gc_power_timeseries(extended_plots_path, scenario) + plot_active_rotations(extended_plots_path, scenario, schedule) + # revert logging override logging.disable(logging.NOTSET) def generate(schedule, scenario, args): - """ Generates all output files/ plots and saves them in the args.results_directory. + """ Generates all output files and saves them in the args.results_directory. :param schedule: Driving schedule for the simulation. :type schedule: simba.Schedule @@ -195,17 +214,6 @@ def generate(schedule, scenario, args): args.propagate_mode_errors=True """ - if args.extended_output_plots: - # create directory for extended plots - extended_plots_path = args.results_directory.joinpath("extended_plots") - extended_plots_path.mkdir(parents=True, exist_ok=True) - - bus_type_distribution_consumption_rotation(extended_plots_path, schedule) - bus_type_distribution_route_rotation(extended_plots_path, schedule) - charge_type_proportion(extended_plots_path, scenario, schedule) - gc_power_time_overview(extended_plots_path, scenario) - active_rotations(extended_plots_path, scenario, schedule) - # generate simulation_timeseries.csv, simulation.json and vehicle_socs.csv in SpiceEV # re-route output paths file_path = args.results_directory / "vehicle_socs.csv" @@ -228,8 +236,8 @@ def generate(schedule, scenario, args): # generate gc overview generate_gc_overview(schedule, scenario, args) - # save plots as png and pdf - generate_plots(scenario, args) + # save plots as png and pdf, includes optional extended plots + generate_plots(schedule, scenario, args) # calculate SOCs for each rotation rotation_infos = [] @@ -304,23 +312,13 @@ def generate(schedule, scenario, args): except ValueError: # Some strings cannot be cast to int and throw a value error rotations = sorted(rotation_socs.keys(), key=lambda k: k) - data = [["time"] + rotations] + # count active rotations for each timestep + num_active_rotations = count_active_rotations(scenario, schedule) + data = [["time"] + rotations + ['# active rotations']] for i in range(scenario.n_intervals): t = sim_start_time + i * scenario.interval socs = [str(rotation_socs[k][i]) for k in rotations] - data.append([str(t)] + socs) - - # add active rotations column to rotation_socs.csv - active_vehicles = [len(scenario.components.vehicles)] * scenario.n_intervals - depot_stations = [station for station in scenario.components.grid_connectors - if schedule.stations[station]["type"] == "deps"] - for station_name in depot_stations: - station_ts = scenario.connChargeByTS[station_name] - for index, step in enumerate(station_ts): - active_vehicles[index] -= len(step) - data[0].append("# active_rotations") - for i in range(len(data)): - data[i].append(active_vehicles[i-1]) + data.append([str(t)] + socs + [num_active_rotations[i]]) file_path = args.results_directory / "rotation_socs.csv" if vars(args).get("scenario_name"): @@ -357,179 +355,177 @@ def generate(schedule, scenario, args): logging.info(f"Plots and output files saved in {args.results_directory}") -def bus_type_distribution_route_rotation(extended_plots_path, schedule): - """Plots the distribution of bus types in consumption brackets as a stacked bar chart. +def write_csv(data: Iterable, file_path, propagate_errors=False): + """ Write iterable data to CSV file. - :param extended_plots_path: Path argument which combines args.results_directory - and string 'extended_plots' - :type extended_plots_path: PosixPath - :param schedule: Driving schedule for the simulation. schedule.rotations are used - :type schedule: eBus-Toolbox.Schedule + Errors are caught if propagate_errors=False. If data is None, no file is created. + + :param data: data which is written to a file + :type data: Iterable + :param file_path: file_path for file creation + :type file_path: str or Path + :param propagate_errors: should errors be propagated? + :type propagate_errors: bool + :raises Exception: if file cannot be written and propagate_errors=True """ + if data is None: + logging.debug(f"Writing to {file_path} aborted since no data is passed.") + return + try: + with open_for_csv(file_path) as f: + csv_writer = csv.writer(f) + for row in data: + csv_writer.writerow(row) + except Exception as e: + logging.warning(f"Writing to {file_path} failed due to {str(e)}") + if propagate_errors: + raise + + +# ##### EXTENDED PLOTTING ##### # + + +def plot_distance_per_rotation_distribution(extended_plots_path, schedule): + """Plots the distribution of bus types in distance brackets as a stacked bar chart. + + :param extended_plots_path: directory to save plot to + :type extended_plots_path: Path + :param schedule: Driving schedule for the simulation, schedule.rotations are used + :type schedule: eBus-Toolbox.Schedule + """ step = 50 - max_route = int(max([schedule.rotations[rot].distance / 1000. for rot in schedule.rotations])) - bin_number = max_route // step + 1 - labels = [f"{i - step} - {i}" for i in range(step, bin_number * step + step, step) if i > 0] - bins = {v_types: [0 for _ in range(bin_number)] for v_types in schedule.vehicle_types} + max_route = int(max([schedule.rotations[rot].distance / 1000 for rot in schedule.rotations])) + num_bins = max_route // step + 1 # number of bars in plot + labels = [f"{i*step} - {(i+1)*step}" for i in range(num_bins)] + # init bins: track distance bins for each vehicle type individually + bins = {v_types: [0]*num_bins for v_types in schedule.vehicle_types} # fill bins with rotations for rot in schedule.rotations: - for v_type in schedule.vehicle_types: - if schedule.rotations[rot].vehicle_type == v_type: - position = int((schedule.rotations[rot].distance / 1000.) // step) - bins[v_type][position] += 1 + position = int((schedule.rotations[rot].distance / 1000) // step) + bins[schedule.rotations[rot].vehicle_type][position] += 1 + # plot fig, ax = plt.subplots() - bar_bottom = [0 for _ in range(bin_number)] + bar_bottom = [0] * num_bins for v_type in schedule.vehicle_types: ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) - for i in range(bin_number): + for i in range(num_bins): bar_bottom[i] += bins[v_type][i] - ax.set_xlabel('Strecke in km') - ax.set_ylabel('Anzahl der Umläufe') - ax.set_title('Verteilung der Bustypen über die Streckenlänge der Umläufe') - ax.legend() - fig.autofmt_xdate() + ax.set_xlabel('Distance [km]') + plt.xticks(rotation=30) # slant labels for better readability + ax.set_ylabel('Number of rotations') + ax.yaxis.get_major_locator().set_params(integer=True) ax.yaxis.grid(True) + ax.set_title('Distribution of bus types over rotation distance') + ax.legend() plt.savefig(extended_plots_path / "distribution_bustypes_route_rotations") plt.close() -def bus_type_distribution_consumption_rotation(extended_plots_path, schedule): +def plot_consumption_per_rotation_distribution(extended_plots_path, schedule): """Plots the distribution of bus types in consumption brackets as a stacked bar chart. - :param extended_plots_path: Path argument which combines args.results_directory - and string 'extended_plots' - :type extended_plots_path: PosixPath - :param schedule: Driving schedule for the simulation. schedule.rotations are used + :param extended_plots_path: directory to save plot to + :type extended_plots_path: Path + :param schedule: Driving schedule for the simulation, schedule.rotations are used :type schedule: eBus-Toolbox.Schedule """ step = 50 - # get bin_number + # get number of bins max_con = int(max([schedule.rotations[rot].consumption for rot in schedule.rotations])) - if max_con % step < step/2: - bin_number = ((max_con // step) * step) - else: - bin_number = (max_con // step) * step + step - labels = [f"{i - step} - {i}" for i in range(step, bin_number+step, step) if i > 0] - bins = {v_types: [0 for _ in range(bin_number // step)] for v_types in schedule.vehicle_types} + num_bins = max_con // step + 1 # number of bars in plot + labels = [f"{i*step} - {(i+1)*step}" for i in range(num_bins)] + # init bins: track consumption bins for each vehicle type individually + bins = {v_types: [0]*num_bins for v_types in schedule.vehicle_types} # fill bins with rotations for rot in schedule.rotations: - for v_type in schedule.vehicle_types: - if schedule.rotations[rot].vehicle_type == v_type: - position = int(schedule.rotations[rot].consumption // step) - if position >= bin_number/step: - position -= bin_number // step - 1 - bins[v_type][position] += 1 # index out of range - break + position = int(schedule.rotations[rot].consumption // step) + bins[schedule.rotations[rot].vehicle_type][position] += 1 + # plot fig, ax = plt.subplots() - bar_bottom = [0 for _ in range(bin_number//step)] + bar_bottom = [0] * num_bins for v_type in schedule.vehicle_types: ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) - for i in range(bin_number//step): + for i in range(num_bins): bar_bottom[i] += bins[v_type][i] - ax.set_xlabel('Energieverbrauch in kWh') - ax.set_ylabel('Anzahl der Umläufe') - ax.set_title('Verteilung der Bustypen über den Energieverbrauch der Umläufe') - ax.legend() - fig.autofmt_xdate() + ax.set_xlabel('Energy consumption [kWh]') + plt.xticks(rotation=30) + ax.set_ylabel('Number of rotations') + ax.yaxis.get_major_locator().set_params(integer=True) ax.yaxis.grid(True) + ax.set_title('Distribution of bus types over rotation energy consumption') + ax.legend() plt.savefig(extended_plots_path / "distribution_bustypes_consumption_rotations") plt.close() -def charge_type_proportion(extended_plots_path, scenario, schedule): - """Plots the absolute number of rotations distributed by charging types on a bar chart. +def plot_charge_type_distribution(extended_plots_path, scenario, schedule): + """Plots the number of rotations of each charging type in a bar chart. - :param extended_plots_path: Path argument which combines args.results_directory - and string 'extended_plots' - :type extended_plots_path: PosixPath + :param extended_plots_path: directory to save plot to + :type extended_plots_path: Path :param scenario: Scenario for with to generate timeseries. :type scenario: spice_ev.Scenario :param schedule: Driving schedule for the simulation. schedule.rotations are used :type schedule: eBus-Toolbox.Schedule """ - # get plotting data + # count charging types (also with regard to negative rotations) charging_types = {'oppb': 0, 'oppb_neg': 0, 'depb': 0, 'depb_neg': 0} negative_rotations = schedule.get_negative_rotations(scenario) for rot in schedule.rotations: - if schedule.rotations[rot].charging_type == 'oppb': - if rot in negative_rotations: - charging_types['oppb_neg'] += 1 - else: - charging_types['oppb'] += 1 - elif schedule.rotations[rot].charging_type == 'depb': - if rot in negative_rotations: - charging_types['depb_neg'] += 1 - else: - charging_types['depb'] += 1 - else: - print("Unknown charging type: ", schedule.rotations[rot].charging_type) + ct = schedule.rotations[rot].charging_type + if rot in negative_rotations: + ct += '_neg' + try: + charging_types[ct] += 1 + except KeyError: + logging.error(f"Rotation {rot}: unknown charging type: '{ct}'") + # plot fig, ax = plt.subplots() bars1 = ax.bar( - ["Gelegenheitslader", "Depotlader"], + ["Opportunity", "Depot"], [charging_types["oppb"], charging_types["depb"]], - color=["#6495ED", "#6495ED"], + color=["#66CDAA", "#66CDAA"], ) bars2 = ax.bar( - ["Gelegenheitslader", "Depotlader"], + ["Opportunity", "Depot"], [charging_types["oppb_neg"], charging_types["depb_neg"]], - color=["#66CDAA", "#66CDAA"], + color=["#6495ED", "#6495ED"], bottom=[charging_types["oppb"], charging_types["depb"]], ) - ax.bar_label(bars1, labels=[ - f"{charging_types['oppb']} oppb", - f"{charging_types['depb']} depb", - ]) - ax.bar_label(bars2, labels=[ - f"{charging_types['oppb_neg']} oppb_neg", - f"{charging_types['depb_neg']} depb_neg", - ]) - - ax.set_xlabel("Ladetyp") - ax.set_ylabel("Umläufe") - ax.set_title("Verteilung von Gelegenheitslader, Depotlader") + # create labels with counts + # create empty labels for empty bins + labels = [ + [ + f"{ct}{suffix}: {charging_types[f'{ct}{suffix}']}" + if charging_types[f'{ct}{suffix}'] > 0 else "" + for ct in ['oppb', 'depb'] + ] for suffix in ['', '_neg'] + ] + ax.bar_label(bars1, labels=labels[0], label_type='center') # oppb, depb + ax.bar_label(bars2, labels=labels[1], label_type='center') # oppb_neg, depb_neg + ax.set_xlabel("Charging type") + ax.set_ylabel("Number of rotations") ax.yaxis.grid(True) - plt.savefig(extended_plots_path / "charge_type_proportion") + ax.yaxis.get_major_locator().set_params(integer=True) + ax.legend(["successful rotations", "negative rotations"]) + ax.set_title("Distribution of opportunity and depot charging") + plt.savefig(extended_plots_path / "charge_types") plt.close() -def gc_power_time_overview_example(args, scenario): - - gc_list = list(scenario.constants.grid_connectors.keys()) - for gc in gc_list: - # data - ts = getattr(scenario, f"{gc}_timeseries") - time = ts["time"] - total = ts["grid power [kW]"] - feed_in = ts["feed-in [kW]"] - ext_load = ts["ext.load [kW]"] - cs = ts["sum CS power"] - - # plot - plt.plot(time, total, label="total") - plt.plot(time, feed_in, label="feed_in") - plt.plot(time, ext_load, label="ext_load") - plt.plot(time, cs, label="CS") - plt.legend() - plt.xticks(rotation=45) - - plt.savefig(args.results_directory / f"{gc}_power_time_overview") - plt.clf() - - -def gc_power_time_overview(extended_plots_path, scenario): +def plot_gc_power_timeseries(extended_plots_path, scenario): """Plots the different loads (total, feedin, external) of all grid connectors. - :param extended_plots_path: Path argument which combines args.results_directory - and string 'extended_plots' - :type extended_plots_path: PosixPath + :param extended_plots_path: directory to save plot to + :type extended_plots_path: Path :param scenario: Provides the data for the grid connectors over time. :type scenario: spice_ev.Scenario """ @@ -548,100 +544,96 @@ def gc_power_time_overview(extended_plots_path, scenario): "bat. stored energy [kWh]", ] + has_battery_column = False + + # find time column time_index = agg_ts["header"].index("time") time_values = [row[time_index] for row in agg_ts["timeseries"]] - for index, header in enumerate(headers): + for header_index, header in enumerate(headers): try: - header_index = agg_ts["header"].index(header) - header_values = [row[header_index] for row in agg_ts["timeseries"]] - - if header == "bat. stored energy [kWh]": - ax2 = ax.twinx() - ax2.set_ylabel("Power in kWh") - next_color = plt.rcParams['axes.prop_cycle'].by_key()["color"][index] - ax2.plot(time_values, header_values, label=header, c=next_color, - linestyle="dashdot") - ax2.legend() - ax2.tick_params(axis='x', rotation=30) - fig.set_size_inches(8, 4.8) - else: - ax.plot(time_values, header_values, label=header) + # try to find column with current header + idx = agg_ts["header"].index(header) + header_values = [row[idx] for row in agg_ts["timeseries"]] except ValueError: + # column does not exist continue - try: - ax2.set_ylim(ax.get_ylim()) - except NameError: - continue + + if header == "bat. stored energy [kWh]": + has_battery_column = True + # special plot for battery: same subplot, different y-axis + ax2 = ax.twinx() + ax2.set_ylabel("stored battery energy [kWh]") + # get next color from color cycle (just plotting would start with first color) + next_color = plt.rcParams['axes.prop_cycle'].by_key()["color"][header_index] + ax2.plot( + time_values, header_values, + label=header, c=next_color, linestyle="dashdot") + ax2.legend() + fig.set_size_inches(8, 4.8) + else: + # normal (non-battery) plot + ax.plot(time_values, header_values, label=header) + + if has_battery_column: + # align y axis so that 0 is shared + # (limits not necessary, as power and energy can't be compared directly) + ax1_ylims = ax.axes.get_ylim() + ax1_yratio = ax1_ylims[0] / ax1_ylims[1] + ax2_ylims = ax2.axes.get_ylim() + ax2_yratio = ax2_ylims[0] / ax2_ylims[1] + if ax1_yratio < ax2_yratio: + ax2.set_ylim(bottom=ax2_ylims[1]*ax1_yratio) + else: + ax.set_ylim(bottom=ax1_ylims[1]*ax2_yratio) + plt.tight_layout() ax.legend() - ax.tick_params(axis='x', rotation=30) - ax.set_ylabel("Power in kW") + plt.xticks(rotation=30) + ax.set_ylabel("Power [kW]") ax.set_title(f"Power: {gc}") ax.grid(color='gray', linestyle='-') - gc_cleaned = gc.replace("/", "").replace(".", "") - plt.savefig(extended_plots_path / f"{gc_cleaned}_power_time_overview.png") + # xaxis are datetime strings + ax.set_xlim(time_values[0], time_values[-1]) + ax.tick_params(axis='x', rotation=30) + + plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_time_overview.png") plt.close(fig) -def active_rotations(extended_plots_path, scenario, schedule): - """Generate a plot where the number of active rotations is shown. +def count_active_rotations(scenario, schedule): + num_active_rotations = [0] * scenario.n_intervals + for rotation in schedule.rotations.values(): + ts_start = int((rotation.departure_time - scenario.start_time) / scenario.interval) + ts_end = ceil((rotation.arrival_time - scenario.start_time) / scenario.interval) + # ignore rotations after scenario end + ts_end = min(ts_end, scenario.n_intervals) + for ts_idx in range(ts_start, ts_end): + num_active_rotations[ts_idx] += 1 + return num_active_rotations - :param extended_plots_path: Path argument which combines args.results_directory - and string 'extended_plots' - :type extended_plots_path: PosixPath - :param schedule: Driving schedule for the simulation. schedule.rotations are used - :type schedule: eBus-Toolbox.Schedule + +def plot_active_rotations(extended_plots_path, scenario, schedule): + """Generate a plot with number of active rotations over time. + + :param extended_plots_path: directory to save plot to + :type extended_plots_path: Path :param scenario: Provides the data for the grid connectors over time. :type scenario: spice_ev.Scenario + :param schedule: Driving schedule for the simulation. schedule.rotations are used + :type schedule: eBus-Toolbox.Schedule """ - ts = [ - scenario.start_time if i == 0 else scenario.start_time + scenario.interval * i - for i in range(scenario.n_intervals) - ] - active_vehicles = [len(scenario.components.vehicles)] * scenario.n_intervals - depot_stations = [ - station for station in scenario.components.grid_connectors - if schedule.stations[station]["type"] == "deps" - ] - for station_name in depot_stations: - station_ts = scenario.connChargeByTS[station_name] - for index, step in enumerate(station_ts): - active_vehicles[index] -= len(step) - - plt.plot(ts, active_vehicles) - plt.ylabel("Number of active Vehicles") - plt.title("Active Rotations") + ts = [scenario.start_time + scenario.interval * i for i in range(scenario.n_intervals)] + num_active_rotations = count_active_rotations(scenario, schedule) + plt.plot(ts, num_active_rotations) + ax = plt.gca() + ax.xaxis_date() + ax.set_xlim(ts[0], ts[-1]) plt.xticks(rotation=30) + plt.ylabel("Number of active rotations") + ax.yaxis.get_major_locator().set_params(integer=True) plt.grid(axis="y") + plt.title("Active Rotations") plt.savefig(extended_plots_path / "active_rotations") plt.close() - - -def write_csv(data: Iterable, file_path, propagate_errors=False): - """ Write iterable data to CSV file. - - Errors are caught if propagate_errors=False. If data is None, no file is created. - - :param data: data which is written to a file - :type data: Iterable - :param file_path: file_path for file creation - :type file_path: str or Path - :param propagate_errors: should errors be propagated? - :type propagate_errors: bool - :raises Exception: if file cannot be written and propagate_errors=True - """ - - if data is None: - logging.debug(f"Writing to {file_path} aborted since no data is passed.") - return - try: - with open_for_csv(file_path) as f: - csv_writer = csv.writer(f) - for row in data: - csv_writer.writerow(row) - except Exception as e: - logging.warning(f"Writing to {file_path} failed due to {str(e)}") - if propagate_errors: - raise From c8641be7b863ab05126de10a478e36c34fbd157f Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 13:41:08 +0200 Subject: [PATCH 22/41] extended plots: better histograms, labels and tests --- simba/report.py | 105 ++++++++++++++++++++++++++--------------- simba/util.py | 2 +- tests/test_simulate.py | 6 +++ 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/simba/report.py b/simba/report.py index a44c7d17..4e5dc8ea 100644 --- a/simba/report.py +++ b/simba/report.py @@ -385,6 +385,52 @@ def write_csv(data: Iterable, file_path, propagate_errors=False): # ##### EXTENDED PLOTTING ##### # +def prepare_histogram(rotations, schedule): + """ Find suitable number of histogram bins for given rotation values. + + :param rotations: Rotation values to create histogram for. ID -> value + :type rotations: dict + :param schedule: Driving schedule + :type schedule: simba.schedule.Schedule + :return: histogram bins, labels + """ + # find suitable step size / number of bins + min_value = int(min(rotations.values())) + max_value = int(max(rotations.values())) + # maximum number of bins (some may be empty): between 1 and 20, optimally half of rotations + max_num_bins = min(max(len(rotations) / 2, 1), 20) + steps = [1, 2.5, 5] # extended to 10, 25, 50, 100, 250, ... + idx = 0 + mult = 1 + while True: + step = steps[idx] * mult + min_bin = (min_value // step) * step + num_bins = int((max_value - min_bin) // step + 1) + if num_bins <= max_num_bins: + # first step with large enough step size / small enough number of bins: + # use this step size + if step != 2.5: + # step size is integer: cast to int for better labels + step = int(step) + min_bin = int(min_bin) + break + # too many bins: increase step size + idx = (idx + 1) % len(steps) + # all steps iterated: append a zero, try again + mult = mult if idx else mult * 10 + + # suitable step size found + labels = [f"{min_bin + i*step} - {min_bin + (i+1)*step}" for i in range(num_bins)] + # init bins: track bins for each vehicle type individually + bins = {v_types: [0]*num_bins for v_types in schedule.vehicle_types} + + # fill bins with rotations + for rot, value in rotations.items(): + position = int((value - min_bin) // step) + bins[schedule.rotations[rot].vehicle_type][position] += 1 + + return bins, labels + def plot_distance_per_rotation_distribution(extended_plots_path, schedule): """Plots the distribution of bus types in distance brackets as a stacked bar chart. @@ -392,35 +438,27 @@ def plot_distance_per_rotation_distribution(extended_plots_path, schedule): :param extended_plots_path: directory to save plot to :type extended_plots_path: Path :param schedule: Driving schedule for the simulation, schedule.rotations are used - :type schedule: eBus-Toolbox.Schedule + :type schedule: simba.schedule.Schedule """ - step = 50 - max_route = int(max([schedule.rotations[rot].distance / 1000 for rot in schedule.rotations])) - num_bins = max_route // step + 1 # number of bars in plot - labels = [f"{i*step} - {(i+1)*step}" for i in range(num_bins)] - # init bins: track distance bins for each vehicle type individually - bins = {v_types: [0]*num_bins for v_types in schedule.vehicle_types} - - # fill bins with rotations - for rot in schedule.rotations: - position = int((schedule.rotations[rot].distance / 1000) // step) - bins[schedule.rotations[rot].vehicle_type][position] += 1 + distances = {rot: schedule.rotations[rot].distance / 1000 for rot in schedule.rotations} + bins, labels = prepare_histogram(distances, schedule) # plot fig, ax = plt.subplots() - bar_bottom = [0] * num_bins + bar_bottom = [0] * len(labels) for v_type in schedule.vehicle_types: ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) - for i in range(num_bins): + for i in range(len(labels)): bar_bottom[i] += bins[v_type][i] ax.set_xlabel('Distance [km]') plt.xticks(rotation=30) # slant labels for better readability ax.set_ylabel('Number of rotations') ax.yaxis.get_major_locator().set_params(integer=True) ax.yaxis.grid(True) - ax.set_title('Distribution of bus types over rotation distance') + ax.set_title('Distribution of rotation length per vehicle type') ax.legend() - plt.savefig(extended_plots_path / "distribution_bustypes_route_rotations") + plt.tight_layout() + plt.savefig(extended_plots_path / "distribution_distance.png") plt.close() @@ -430,37 +468,27 @@ def plot_consumption_per_rotation_distribution(extended_plots_path, schedule): :param extended_plots_path: directory to save plot to :type extended_plots_path: Path :param schedule: Driving schedule for the simulation, schedule.rotations are used - :type schedule: eBus-Toolbox.Schedule + :type schedule: simba.schedule.Schedule """ - - step = 50 - # get number of bins - max_con = int(max([schedule.rotations[rot].consumption for rot in schedule.rotations])) - num_bins = max_con // step + 1 # number of bars in plot - labels = [f"{i*step} - {(i+1)*step}" for i in range(num_bins)] - # init bins: track consumption bins for each vehicle type individually - bins = {v_types: [0]*num_bins for v_types in schedule.vehicle_types} - - # fill bins with rotations - for rot in schedule.rotations: - position = int(schedule.rotations[rot].consumption // step) - bins[schedule.rotations[rot].vehicle_type][position] += 1 + consumption = {rot: schedule.rotations[rot].consumption for rot in schedule.rotations} + bins, labels = prepare_histogram(consumption, schedule) # plot fig, ax = plt.subplots() - bar_bottom = [0] * num_bins + bar_bottom = [0] * len(labels) for v_type in schedule.vehicle_types: ax.bar(labels, bins[v_type], width=0.9, label=v_type, bottom=bar_bottom) - for i in range(num_bins): + for i in range(len(labels)): bar_bottom[i] += bins[v_type][i] ax.set_xlabel('Energy consumption [kWh]') plt.xticks(rotation=30) ax.set_ylabel('Number of rotations') ax.yaxis.get_major_locator().set_params(integer=True) ax.yaxis.grid(True) - ax.set_title('Distribution of bus types over rotation energy consumption') + ax.set_title('Distribution of energy consumption of rotations per vehicly type') ax.legend() - plt.savefig(extended_plots_path / "distribution_bustypes_consumption_rotations") + plt.tight_layout() + plt.savefig(extended_plots_path / "distribution_consumption") plt.close() @@ -472,7 +500,7 @@ def plot_charge_type_distribution(extended_plots_path, scenario, schedule): :param scenario: Scenario for with to generate timeseries. :type scenario: spice_ev.Scenario :param schedule: Driving schedule for the simulation. schedule.rotations are used - :type schedule: eBus-Toolbox.Schedule + :type schedule: simba.schedule.Schedule """ # count charging types (also with regard to negative rotations) charging_types = {'oppb': 0, 'oppb_neg': 0, 'depb': 0, 'depb_neg': 0} @@ -516,7 +544,7 @@ def plot_charge_type_distribution(extended_plots_path, scenario, schedule): ax.yaxis.grid(True) ax.yaxis.get_major_locator().set_params(integer=True) ax.legend(["successful rotations", "negative rotations"]) - ax.set_title("Distribution of opportunity and depot charging") + ax.set_title("Feasability of rotations per charging type") plt.savefig(extended_plots_path / "charge_types") plt.close() @@ -598,7 +626,8 @@ def plot_gc_power_timeseries(extended_plots_path, scenario): ax.set_xlim(time_values[0], time_values[-1]) ax.tick_params(axis='x', rotation=30) - plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_time_overview.png") + plt.tight_layout() + plt.savefig(extended_plots_path / f"{sanitize(gc)}_overview.png") plt.close(fig) @@ -622,7 +651,7 @@ def plot_active_rotations(extended_plots_path, scenario, schedule): :param scenario: Provides the data for the grid connectors over time. :type scenario: spice_ev.Scenario :param schedule: Driving schedule for the simulation. schedule.rotations are used - :type schedule: eBus-Toolbox.Schedule + :type schedule: simba.schedule.Schedule """ ts = [scenario.start_time + scenario.interval * i for i in range(scenario.n_intervals)] num_active_rotations = count_active_rotations(scenario, schedule) diff --git a/simba/util.py b/simba/util.py index dcecc07f..b8d88e0c 100644 --- a/simba/util.py +++ b/simba/util.py @@ -420,7 +420,7 @@ def get_parser(): help='Remove rotations from schedule that violate assumptions. ') parser.add_argument('--show-plots', action='store_true', help='show plots for users to view in "report" mode') - parser.add_argument('--extended_output_plots', action='store_true', + parser.add_argument('--extended-output-plots', action='store_true', help='show extended plots') parser.add_argument('--propagate-mode-errors', action='store_true', help='Re-raise errors instead of continuing during simulation modes') diff --git a/tests/test_simulate.py b/tests/test_simulate.py index b16a38e0..46ffb66f 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -175,6 +175,12 @@ def test_extended_plot(self, tmp_path): with warnings.catch_warnings(): warnings.simplefilter("ignore") simulate(args) + for expected_file in [ + "active_rotations.png", "charge_types.png", "Station-0_overview.png", + "distribution_consumption.png", "distribution_distance.png" + ]: + assert (tmp_path / f"report_1/extended_plots/{expected_file}").exists(), ( + f"{expected_file} not found in extended plots") def test_create_trips_in_report(self, tmp_path): # create_trips_in_report option: must generate valid input trips.csv From 0f718b60f3ac304de2bae163ee8e84c845ede814 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 13:50:20 +0200 Subject: [PATCH 23/41] extended charge type plot: use default colors --- simba/report.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/simba/report.py b/simba/report.py index 4e5dc8ea..f4c09ef6 100644 --- a/simba/report.py +++ b/simba/report.py @@ -519,12 +519,10 @@ def plot_charge_type_distribution(extended_plots_path, scenario, schedule): bars1 = ax.bar( ["Opportunity", "Depot"], [charging_types["oppb"], charging_types["depb"]], - color=["#66CDAA", "#66CDAA"], ) bars2 = ax.bar( ["Opportunity", "Depot"], [charging_types["oppb_neg"], charging_types["depb_neg"]], - color=["#6495ED", "#6495ED"], bottom=[charging_types["oppb"], charging_types["depb"]], ) # create labels with counts From 65920b231c09645509821500c66b28b4309eb101 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 16:16:51 +0200 Subject: [PATCH 24/41] zip option --- simba/__main__.py | 7 +++++++ simba/util.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/simba/__main__.py b/simba/__main__.py index d5e848c1..b5e5f59d 100644 --- a/simba/__main__.py +++ b/simba/__main__.py @@ -1,6 +1,7 @@ from datetime import datetime import logging from pathlib import Path +import shutil from simba import simulate, util @@ -38,4 +39,10 @@ logging.error(e) raise finally: + if args.zip_output and args.output_directory is not None and args.output_directory.exists(): + # compress output directory after simulation + # generate .zip at location of original output directory + shutil.make_archive(args.output_directory, 'zip', args.output_directory) + # remove original output directory + shutil.rmtree(args.output_directory) logging.shutdown() diff --git a/simba/util.py b/simba/util.py index 85c8100c..b999c3c7 100644 --- a/simba/util.py +++ b/simba/util.py @@ -458,6 +458,8 @@ def get_parser(): parser.add_argument('--rotation-filter-variable', default=None, choices=[None, 'include', 'exclude'], help='set mode for filtering schedule rotations') + parser.add_argument('--zip-output', '-z', action='store_true', + help='compress output folder after simulation') # #### Charging strategy ##### parser.add_argument('--preferred-charging-type', '-pct', default='depb', From 495ce3e768a9a41885653111ee59fa3908739e51 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 16:23:20 +0200 Subject: [PATCH 25/41] fix typos --- simba/report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simba/report.py b/simba/report.py index f4c09ef6..8b766ca8 100644 --- a/simba/report.py +++ b/simba/report.py @@ -485,7 +485,7 @@ def plot_consumption_per_rotation_distribution(extended_plots_path, schedule): ax.set_ylabel('Number of rotations') ax.yaxis.get_major_locator().set_params(integer=True) ax.yaxis.grid(True) - ax.set_title('Distribution of energy consumption of rotations per vehicly type') + ax.set_title('Distribution of energy consumption of rotations per vehicle type') ax.legend() plt.tight_layout() plt.savefig(extended_plots_path / "distribution_consumption") @@ -542,7 +542,7 @@ def plot_charge_type_distribution(extended_plots_path, scenario, schedule): ax.yaxis.grid(True) ax.yaxis.get_major_locator().set_params(integer=True) ax.legend(["successful rotations", "negative rotations"]) - ax.set_title("Feasability of rotations per charging type") + ax.set_title("Feasibility of rotations per charging type") plt.savefig(extended_plots_path / "charge_types") plt.close() @@ -625,7 +625,7 @@ def plot_gc_power_timeseries(extended_plots_path, scenario): ax.tick_params(axis='x', rotation=30) plt.tight_layout() - plt.savefig(extended_plots_path / f"{sanitize(gc)}_overview.png") + plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_overview.png") plt.close(fig) From d1a5c97507bf16596abd0d671f8bc859a455c0c0 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 16:27:54 +0200 Subject: [PATCH 26/41] fix test --- tests/test_simulate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_simulate.py b/tests/test_simulate.py index 46ffb66f..be41ef28 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -176,7 +176,7 @@ def test_extended_plot(self, tmp_path): warnings.simplefilter("ignore") simulate(args) for expected_file in [ - "active_rotations.png", "charge_types.png", "Station-0_overview.png", + "active_rotations.png", "charge_types.png", "Station-0_power_overview.png", "distribution_consumption.png", "distribution_distance.png" ]: assert (tmp_path / f"report_1/extended_plots/{expected_file}").exists(), ( From 68e6ac7b59d11c8da4361f84df6448a86fc2bdcc Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 16:51:31 +0200 Subject: [PATCH 27/41] fix documentation --- docs/source/modes.rst | 2 +- simba/schedule.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/source/modes.rst b/docs/source/modes.rst index 12640d35..85ed29dd 100644 --- a/docs/source/modes.rst +++ b/docs/source/modes.rst @@ -79,7 +79,7 @@ Now, only rotations are left that are non-negative when viewed alone, but might In the end, the largest number of rotations that produce a non-negative result when taken together is returned as the optimized scenario. Recombination: split negative depot rotations into smaller rotations -------------- +-------------------------------------------------------------------- :: mode = split_negative_depb diff --git a/simba/schedule.py b/simba/schedule.py index 341e3ba3..60dd8354 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -211,15 +211,16 @@ def check_consistency(cls, schedule): def run(self, args, mode="distributed"): """Runs a schedule without assigning vehicles. - For external usage the core run functionality is accessible through this function. It - allows for defining a custom-made assign_vehicles method for the schedule. + For external usage the core run functionality is accessible through this function. + It allows for defining a custom-made assign_vehicles method for the schedule. + :param args: used arguments are rotation_filter, path to rotation ids, and rotation_filter_variable that sets mode (options: include, exclude) :type args: argparse.Namespace - :param mode: option of "distributed" or "greedy" + :param mode: SpiceEV strategy name :type mode: str :return: scenario - :rtype spice_ev.Scenario + :rtype: spice_ev.Scenario """ # Make sure all rotations have an assigned vehicle assert all([rot.vehicle_id is not None for rot in self.rotations.values()]) @@ -350,8 +351,9 @@ def assign_vehicles_w_min_recharge_soc(self): def assign_vehicles_custom(self, vehicle_assigns: Iterable[dict]): """ Assign vehicles on a custom basis. - Assign vehicles based on a datasource, containing all rotations, their vehicle_ids and - desired start socs. + Assign vehicles based on a datasource, + containing all rotations, their vehicle_ids and desired start socs. + :param vehicle_assigns: Iterable of dict with keys rotation_id, vehicle_id and start_soc for each rotation :type vehicle_assigns: Iterable[dict] @@ -1127,6 +1129,7 @@ def update_csv_file_info(file_info, gc_name): - set grid_connector_id - update csv_file path - set start_time and step_duration_s from CSV information if not given + :param file_info: csv information from electrified station :type file_info: dict :param gc_name: station name From 7ea1a5fd67065637f82ab3940619018cb3500490 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Wed, 18 Sep 2024 16:57:42 +0200 Subject: [PATCH 28/41] add documentation for extended_output_plots --- docs/source/simba_features.rst | 5 ++++- docs/source/simulation_parameters.rst | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/simba_features.rst b/docs/source/simba_features.rst index 914a9356..cc392797 100644 --- a/docs/source/simba_features.rst +++ b/docs/source/simba_features.rst @@ -53,7 +53,10 @@ Generate report The generation of the report is implemented as a mode, that can be activated with the keyword "report" in modes (:ref:`report`). The report generates most of the output files. The report can be called any number of times e.g. mode = ["sim", "report", "neg_depb_to_oppb", "report", "service_optimization", "report"]. For each report, a sub-folder is created in the output directory (as defined in the :ref:`config`) named "report_[nr]" with the respective number. -The generation of the report can be modified using the flag "cost_calculation" in :ref:`config`. If this flag is set to true, each report will also generate the file "summary_vehicles_costs.csv". +The generation of the report can be modified using flags in :ref:`config`. + +1. If ``cost_calculation`` is set, each report will also generate the file "summary_vehicles_costs.csv". +2. If ``extended_output_plots`` is set, more plots will be saved to "extended_plots" in the report directory. These plots include the number of active rotations over time, a distribution of charge types, histograms of rotation consumption and distance as well as station-specific power levels. Default outputs ############### diff --git a/docs/source/simulation_parameters.rst b/docs/source/simulation_parameters.rst index cbfba443..3ce450fe 100644 --- a/docs/source/simulation_parameters.rst +++ b/docs/source/simulation_parameters.rst @@ -84,6 +84,10 @@ The example (data/simba.cfg) contains parameter descriptions which are explained - false - Boolean - If activated, plots are displayed with every run of :ref:`report` mode + * - extended_output_plots + - false + - Boolean + - If set, create additional plots when running :ref:`report` mode * - strategy_deps - balanced - SpiceEV Strategies (greedy, balanced, peak_shaving, peak_load_windows, balanced_market) From bfcaa77b8e6614b38aa340c40ea35026bf5e6cc4 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Wed, 18 Sep 2024 17:09:58 +0200 Subject: [PATCH 29/41] Add plots for blocks and vehicle services --- simba/report.py | 148 +++++++++++++++++++++++++++++++++++++++++++++++- simba/util.py | 13 +++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/simba/report.py b/simba/report.py index a44c7d17..000a1c02 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 dcecc07f..e2afdce0 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. From 7f173d6902ecb20a1a9d7680123c33ddeca32cf5 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Thu, 19 Sep 2024 09:20:20 +0200 Subject: [PATCH 30/41] extended plots: set size for active rotations plot --- simba/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simba/report.py b/simba/report.py index 8b766ca8..b9c08a94 100644 --- a/simba/report.py +++ b/simba/report.py @@ -654,6 +654,8 @@ def plot_active_rotations(extended_plots_path, scenario, schedule): ts = [scenario.start_time + scenario.interval * i for i in range(scenario.n_intervals)] num_active_rotations = count_active_rotations(scenario, schedule) plt.plot(ts, num_active_rotations) + fig = plt.gcf() + fig.set_size_inches(8, 4.8) ax = plt.gca() ax.xaxis_date() ax.set_xlim(ts[0], ts[-1]) @@ -662,5 +664,6 @@ def plot_active_rotations(extended_plots_path, scenario, schedule): ax.yaxis.get_major_locator().set_params(integer=True) plt.grid(axis="y") plt.title("Active Rotations") + plt.tight_layout() plt.savefig(extended_plots_path / "active_rotations") plt.close() From c6f358255c4350d7138009fe8aff4764773002d7 Mon Sep 17 00:00:00 2001 From: "stefan.schirmeister" Date: Thu, 19 Sep 2024 10:10:05 +0200 Subject: [PATCH 31/41] fix conflict between logging and output dir removal --- simba/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simba/__main__.py b/simba/__main__.py index b5e5f59d..094d6482 100644 --- a/simba/__main__.py +++ b/simba/__main__.py @@ -39,10 +39,10 @@ logging.error(e) raise finally: + logging.shutdown() if args.zip_output and args.output_directory is not None and args.output_directory.exists(): # compress output directory after simulation # generate .zip at location of original output directory shutil.make_archive(args.output_directory, 'zip', args.output_directory) # remove original output directory shutil.rmtree(args.output_directory) - logging.shutdown() From a70923a47dafa0c64a6514edb75a3e2cc21dd283 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 10:39:40 +0200 Subject: [PATCH 32/41] Make lol and temperatures optional again. make changed config parameter names backwards compatible Simulation can run without temperatures and lol if vehicle_types have constant mileage. vehicle types with mileage look up will fail late. Arguments expanded with deprecated values which replace current values. Warning for deprecation is given. Station Height data lookup will give errors in case station is missing from station_data. in other cases a single warning is given if no station data is provided --- simba/schedule.py | 28 ++++++++++++++++++---------- simba/util.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_schedule.py | 40 ++++++++++++++++++++++++++++++++++++++++ tests/test_simulate.py | 2 ++ 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/simba/schedule.py b/simba/schedule.py index 60dd8354..26afc582 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -103,10 +103,16 @@ def from_datacontainer(cls, data: DataContainer, args): # Add geo data to schedule schedule.station_data = data.station_geo_data + if not schedule.station_data: + logging.warning( + "No station data found for schedule. Height difference for all trips set to 0") + # Add consumption calculator to trip class schedule.consumption_calculator = Consumption.create_from_data_container(data) for trip in data.trip_data: + # De-link DataContainer and Schedule instances + trip = {key: data for key, data in trip.items()} rotation_id = trip['rotation_id'] # Get height difference from station_data @@ -114,18 +120,21 @@ def from_datacontainer(cls, data: DataContainer, args): trip["departure_name"], trip["arrival_name"]) if trip["level_of_loading"] is None: - assert len(data.level_of_loading_data) == 24, "Need 24 entries in level of loading" - trip["level_of_loading"] = util.get_mean_from_hourly_dict( - data.level_of_loading_data, trip["departure_time"], trip["arrival_time"]) + if data.level_of_loading_data: + assert len(data.level_of_loading_data) == 24, \ + "Need 24 entries in level of loading" + trip["level_of_loading"] = util.get_mean_from_hourly_dict( + data.level_of_loading_data, trip["departure_time"], trip["arrival_time"]) else: if not 0 <= trip["level_of_loading"] <= 1: logging.warning("Level of loading is out of range [0,1] and will be clipped.") trip["level_of_loading"] = min(1, max(0, trip["level_of_loading"])) if trip["temperature"] is None: - assert len(data.temperature_data) == 24, "Need 24 entries in temperature data" - trip["temperature"] = util.get_mean_from_hourly_dict( - data.temperature_data, trip["departure_time"], trip["arrival_time"]) + if data.temperature_data: + assert len(data.temperature_data) == 24, "Need 24 entries in temperature data" + trip["temperature"] = util.get_mean_from_hourly_dict( + data.temperature_data, trip["departure_time"], trip["arrival_time"]) if rotation_id not in schedule.rotations.keys(): schedule.rotations.update({ @@ -727,7 +736,7 @@ def get_height_difference(self, departure_name, arrival_name): :return: Height difference. Defaults to 0 if height data is not found. :rtype: float """ - if isinstance(self.station_data, dict): + if isinstance(self.station_data, dict) and self.station_data: station_name = departure_name try: start_height = self.station_data[station_name]["elevation"] @@ -736,9 +745,8 @@ def get_height_difference(self, departure_name, arrival_name): return end_height - start_height except KeyError: logging.error( - f"No elevation data found for {station_name}. Height difference set to 0") - else: - logging.error("No station data found for schedule. Height difference set to 0") + f"No elevation data found for {station_name} in station_data. " + "Height difference set to 0") return 0 def get_negative_rotations(self, scenario): diff --git a/simba/util.py b/simba/util.py index 93feacbe..a829985e 100644 --- a/simba/util.py +++ b/simba/util.py @@ -339,6 +339,30 @@ def get_buffer_time(trip, default=0): return buffer_time +def replace_deprecated_arguments(args): + if args.electrified_stations is not None: + assert args.electrified_stations_path is None + logging.warning("The parameter 'electrified_stations' is deprecated. " + "Use 'electrified_stations_path 'instead.") + args.electrified_stations_path = args.electrified_stations + del args.electrified_stations + + if args.vehicle_types is not None: + assert args.vehicle_types_path is None + logging.warning("The parameter 'vehicle_types' is deprecated. " + "Use 'vehicle_types_path 'instead.") + args.vehicle_types_path = args.vehicle_types + del args.vehicle_types + + if args.cost_parameters_file is not None: + assert args.cost_parameter_path is None + logging.warning("The parameter 'cost_parameter_file' is deprecated. " + "Use 'cost_parameter_path 'instead.") + args.cost_parameter_path = args.cost_parameters_file + del args.cost_parameters_file + return args + + def mutate_args_for_spiceev(args): # arguments relevant to SpiceEV, setting automatically to reduce clutter in config args.margin = 1 @@ -357,6 +381,9 @@ def get_args(): # If a config is provided, the config will overwrite previously parsed arguments set_options_from_config(args, check=parser, verbose=False) + # Check if deprecated arguments were given and change them accordingly + args = replace_deprecated_arguments(args) + # rename special options args.timing = args.eta @@ -528,6 +555,13 @@ def get_parser(): parser.add_argument('--time-windows', metavar='FILE', help='use peak load windows to force lower power ' 'during times of high grid load') + # Deprecated options for downwards compatibility + parser.add_argument('--electrified-stations', default=None, + help='Deprecated use "electrified-stations-path" instead') + parser.add_argument('--vehicle-types', default=None, + help='Deprecated use "vehicle-types-path" instead') + parser.add_argument('--cost-parameters-file', default=None, + help='Deprecated use "cost-parameters-path" instead') parser.add_argument('--config', help='Use config file to set arguments') return parser diff --git a/tests/test_schedule.py b/tests/test_schedule.py index 57a5730b..e5848f93 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -61,6 +61,46 @@ def basic_run(self): class TestSchedule(BasicSchedule): + def test_optional_timeseries(self): + # Test if simulation runs if level of loading and temperature timeseries is not given + sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] + args = util.get_args() + data_container = DataContainer().fill_with_args(args) + vehicle_mileage_path = None + found = False + for vt in data_container.vehicle_types_data.values(): + for ct in vt.values(): + if isinstance(ct["mileage"], str): + vehicle_mileage_path = ct["mileage"] + found = True + break + if found: + break + + sched, args = pre_simulation(args, data_container) + + # Make sure at least a single used vehicle type has mileage lookup. + some_used_vehicle_type = next(iter(sched.rotations.values())).vehicle_type + data_container.vehicle_types_data[some_used_vehicle_type][ + "oppb"]["mileage"] = vehicle_mileage_path + data_container.vehicle_types_data[some_used_vehicle_type][ + "depb"]["mileage"] = vehicle_mileage_path + + # Delete the lol and temp data sources -> pre-simulation should fail, + # since consumption cannot be calculated + data_container.level_of_loading_data = {} + data_container.temperature_data = {} + for trip in data_container.trip_data: + print(trip) + with pytest.raises(Exception): + sched, args = pre_simulation(args, data_container) + + # if all vehicle types have constant consumption, pre-simulation should work + for vt in data_container.vehicle_types_data.values(): + for ct in vt.values(): + ct["mileage"] = 1 + _, _ = pre_simulation(args, data_container) + def test_timestep(self): # Get the parser from util. This way the test is directly coupled to the parser arguments sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] diff --git a/tests/test_simulate.py b/tests/test_simulate.py index 030c7ff4..11c02329 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -65,6 +65,8 @@ def get_args(self): parser.set_defaults(**self.NON_DEFAULT_VALUES) # get all args with default values args, _ = parser.parse_known_args() + args = util.replace_deprecated_arguments(args) + return args def test_basic(self): From afcc9c8e15f76002694384f17f4a90314596d277 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 12:45:32 +0200 Subject: [PATCH 33/41] Fix sorting of block plots. Fix label size --- simba/report.py | 111 ++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 60 deletions(-) diff --git a/simba/report.py b/simba/report.py index 000a1c02..93a740f8 100644 --- a/simba/report.py +++ b/simba/report.py @@ -609,6 +609,25 @@ def plot_gc_power_timeseries(extended_plots_path, scenario): 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) + 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 plot_vehicle_services(schedule, output_path): """Plots the rotations serviced by the same vehicle @@ -624,50 +643,30 @@ def plot_vehicle_services(schedule, output_path): 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(): + 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 - # 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(): + # 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_gen, dense_row_generator) - ax.set_xlabel("Vehicle ID") - ax.set_xlabel("Block") - fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.png") + 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") + ax.tick_params(axis='y', labelsize=8) + fig.tight_layout() + # PDF so Block names stay readable + fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.pdf") plt.close(fig) @@ -685,17 +684,7 @@ def plot_blocks_dense(schedule, output_path): 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(): + def DenseRowGenerator(): # Generates row id by trying to find lowes row with a rotation which arrived arrival_times = [] row_nr = None @@ -712,39 +701,41 @@ def dense_row_generator(): 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") + # 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) + fig.tight_layout() + # PDF so Block names stay readable + fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.pdf") 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)) + # initialize row_generator by yielding None + next(row_generator) + next(color_generator) + + fig, ax = plt.subplots(figsize=(5, len(rotations) * 0.02 + 2)) 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.bar_label(artist, labels=[rotation.id], label_type='center', fontsize=2) 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() + ax.tick_params(axis='x', rotation=30) return fig, ax From 3b78a9d23b1fe5482956e3b46b70e1e5a94832a9 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 14:21:53 +0200 Subject: [PATCH 34/41] Add legend and add png output --- simba/report.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/simba/report.py b/simba/report.py index 93a740f8..27e3cacd 100644 --- a/simba/report.py +++ b/simba/report.py @@ -9,6 +9,7 @@ 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 @@ -619,12 +620,16 @@ def ColorGenerator(vehicle_types): 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": - pass + if value >= 0.5: + value -= 0.3 elif rotation.charging_type == "oppb": - hsv[-1] /= 2 + if value < 0.5: + value += 0.3 else: raise NotImplementedError + hsv[-1] = value color = matplotlib.colors.hsv_to_rgb(hsv) @@ -663,10 +668,21 @@ def VehicleIdRowGenerator(): 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") - ax.tick_params(axis='y', labelsize=8) + # 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, 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") plt.close(fig) @@ -709,32 +725,38 @@ def 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)))) + 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) + ax.set_ylim(0, 125) 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") plt.close(fig) -def create_plot_blocks(rotations, color_generator, row_generator): +def create_plot_blocks(sorted_rotations, color_generator, row_generator): # initialize row_generator by yielding None next(row_generator) next(color_generator) - - fig, ax = plt.subplots(figsize=(5, len(rotations) * 0.02 + 2)) - for rotation in rotations: + 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.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 rotations) - datetime.timedelta(minutes=10), - max(r.arrival_time for r in rotations) + datetime.timedelta(minutes=10)) + 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 From 605d45294174d31cc0ad85ccad7727e980ac0587 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 14:29:23 +0200 Subject: [PATCH 35/41] Add legend to blocks_dense --- simba/report.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/simba/report.py b/simba/report.py index 27e3cacd..3953b7fe 100644 --- a/simba/report.py +++ b/simba/report.py @@ -730,7 +730,16 @@ def DenseRowGenerator(): fig, ax = create_plot_blocks(sorted_rotations, color_generator, row_generator) ax.set_ylabel("Block") ax.yaxis.get_major_locator().set_params(integer=True) - ax.set_ylim(0, 125) + # 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, prop={"size": 7}) + fig.tight_layout() # PDF so Block names stay readable fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.pdf") From 18d9b90be03aeba198791dbd57cc06f9f600d87c Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 14:44:22 +0200 Subject: [PATCH 36/41] Add minimum ncols --- simba/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simba/report.py b/simba/report.py index 3953b7fe..4f0b8a55 100644 --- a/simba/report.py +++ b/simba/report.py @@ -678,7 +678,7 @@ def VehicleIdRowGenerator(): 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, prop={"size": 7}) + 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") @@ -738,7 +738,7 @@ def DenseRowGenerator(): 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, prop={"size": 7}) + ncol=len(handles)//2+1, prop={"size": 7}) fig.tight_layout() # PDF so Block names stay readable From 8ecd6732ad671cf2f1f9f52a8747a8a8c5a0dc1f Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 15:08:16 +0200 Subject: [PATCH 37/41] Add dpi for all plots. Add x grid for box plots --- simba/report.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/simba/report.py b/simba/report.py index 4f0b8a55..289d6841 100644 --- a/simba/report.py +++ b/simba/report.py @@ -15,6 +15,8 @@ from simba import util +DPI = 300 + def open_for_csv(file_path): """ Create a file handle to write to. @@ -182,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=300) 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=300) if args.show_plots: plt.show() @@ -428,7 +430,7 @@ def plot_distance_per_rotation_distribution(extended_plots_path, schedule): ax.yaxis.grid(True) ax.set_title('Distribution of bus types over rotation distance') ax.legend() - plt.savefig(extended_plots_path / "distribution_bustypes_route_rotations") + plt.savefig(extended_plots_path / "distribution_bustypes_route_rotations", dpi=300) plt.close() @@ -468,7 +470,7 @@ def plot_consumption_per_rotation_distribution(extended_plots_path, schedule): ax.yaxis.grid(True) ax.set_title('Distribution of bus types over rotation energy consumption') ax.legend() - plt.savefig(extended_plots_path / "distribution_bustypes_consumption_rotations") + plt.savefig(extended_plots_path / "distribution_bustypes_consumption_rotations", dpi=300) plt.close() @@ -525,7 +527,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("Distribution of opportunity and depot charging") - plt.savefig(extended_plots_path / "charge_types") + plt.savefig(extended_plots_path / "charge_types", dpi=300) plt.close() @@ -606,7 +608,7 @@ def plot_gc_power_timeseries(extended_plots_path, scenario): ax.set_xlim(time_values[0], time_values[-1]) # ax.tick_params(axis='x', rotation=30) - plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_time_overview.png") + plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_time_overview.png", dpi=300) plt.close(fig) @@ -682,7 +684,7 @@ def VehicleIdRowGenerator(): 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") + fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.png", dpi=300) plt.close(fig) @@ -743,7 +745,7 @@ def DenseRowGenerator(): 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") + fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.png", dpi=300) plt.close(fig) @@ -762,6 +764,7 @@ def create_plot_blocks(sorted_rotations, color_generator, row_generator): # 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), @@ -803,5 +806,5 @@ def plot_active_rotations(extended_plots_path, scenario, schedule): ax.yaxis.get_major_locator().set_params(integer=True) plt.grid(axis="y") plt.title("Active Rotations") - plt.savefig(extended_plots_path / "active_rotations") + plt.savefig(extended_plots_path / "active_rotations", dpi=300) plt.close() From 3a06d0abea1b81112d162c6265e31820a4730a62 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Thu, 19 Sep 2024 15:34:14 +0200 Subject: [PATCH 38/41] Handle default value of parser for backwards compatibility --- data/examples/simba.cfg | 6 +++--- simba/util.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/data/examples/simba.cfg b/data/examples/simba.cfg index 27912033..d0dbfa9e 100644 --- a/data/examples/simba.cfg +++ b/data/examples/simba.cfg @@ -10,9 +10,9 @@ input_schedule = data/examples/trips_example.csv # Deactivate storage of output by setting output_directory = null output_directory = data/output/ # Electrified stations (required) -electrified_stations_path = data/examples/electrified_stations.json +electrified_stations = data/examples/electrified_stations.json # Vehicle types (defaults to: ./data/examples/vehicle_types.json) -vehicle_types_path = data/examples/vehicle_types.json +vehicle_types = data/examples/vehicle_types.json # Path to station data with stations heights # (Optional: needed if mileage in vehicle types not constant and inclination should be considered) station_data_path = data/examples/all_stations.csv @@ -25,7 +25,7 @@ level_of_loading_over_day_path = data/examples/default_level_of_loading_over_da # Path to configuration file for the station optimization. Only needed for mode "station_optimization" optimizer_config = data/examples/default_optimizer.cfg # Cost parameters (needed if cost_calculation flag is set to true, see Flag section below) -cost_parameters_path = data/examples/cost_params.json +cost_parameters_file = data/examples/cost_params.json # Path to rotation filter rotation_filter = data/examples/rotation_filter.csv diff --git a/simba/util.py b/simba/util.py index c1dfa2bc..161cba58 100644 --- a/simba/util.py +++ b/simba/util.py @@ -341,24 +341,26 @@ def get_buffer_time(trip, default=0): def replace_deprecated_arguments(args): if args.electrified_stations is not None: - assert args.electrified_stations_path is None + assert args.electrified_stations_path is None, \ + "Multiple electrified stations are not supported." logging.warning("The parameter 'electrified_stations' is deprecated. " "Use 'electrified_stations_path 'instead.") args.electrified_stations_path = args.electrified_stations del args.electrified_stations if args.vehicle_types is not None: - assert args.vehicle_types_path is None logging.warning("The parameter 'vehicle_types' is deprecated. " - "Use 'vehicle_types_path 'instead.") + "Use 'vehicle_types_path 'instead. The value of args.vehicle_types_path " + f"{args.vehicle_types_path} is replaced with {args.vehicle_types}.") args.vehicle_types_path = args.vehicle_types del args.vehicle_types if args.cost_parameters_file is not None: - assert args.cost_parameter_path is None - logging.warning("The parameter 'cost_parameter_file' is deprecated. " - "Use 'cost_parameter_path 'instead.") - args.cost_parameter_path = args.cost_parameters_file + assert args.cost_parameters_path is None, \ + "Multiple cost parameters files are not supported." + logging.warning("The parameter 'cost_parameters_file' is deprecated. " + "Use 'cost_parameters_path 'instead.") + args.cost_parameters_path = args.cost_parameters_file del args.cost_parameters_file return args From c7b808ecb00ec990ea25c9d9d7edc828dd41b011 Mon Sep 17 00:00:00 2001 From: Julian Brendel Date: Thu, 19 Sep 2024 16:06:20 +0200 Subject: [PATCH 39/41] replace 300 with variable KPI --- simba/report.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/simba/report.py b/simba/report.py index 058fcb89..ef5ddbd7 100644 --- a/simba/report.py +++ b/simba/report.py @@ -184,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, dpi=300) + 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, dpi=300) + plt.savefig(file_path_pdf, dpi=DPI) if args.show_plots: plt.show() @@ -468,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", dpi=300) + plt.savefig(extended_plots_path / "distribution_distance.png", dpi=DPI) plt.close() @@ -498,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", dpi=300) + plt.savefig(extended_plots_path / "distribution_consumption", dpi=DPI) plt.close() @@ -553,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", dpi=300) + plt.savefig(extended_plots_path / "charge_types", dpi=DPI) plt.close() @@ -634,7 +634,7 @@ def plot_gc_power_timeseries(extended_plots_path, scenario): ax.set_xlim(time_values[0], time_values[-1]) # ax.tick_params(axis='x', rotation=30) plt.tight_layout() - plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_overview.png", dpi=300) + plt.savefig(extended_plots_path / f"{sanitize(gc)}_power_overview.png", dpi=DPI) plt.close(fig) @@ -710,7 +710,7 @@ def VehicleIdRowGenerator(): 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=300) + fig.savefig(output_path_folder / f"{sanitize(depot)}_vehicle_services.png", dpi=DPI) plt.close(fig) @@ -771,7 +771,7 @@ def DenseRowGenerator(): 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=300) + fig.savefig(output_path_folder / f"{sanitize(depot)}_block_distribution.png", dpi=DPI) plt.close(fig) @@ -835,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", dpi=300) + plt.savefig(extended_plots_path / "active_rotations", dpi=DPI) plt.close() From a91647ab963ffa3081e240d1e24e06415ea72935 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Fri, 20 Sep 2024 10:55:33 +0200 Subject: [PATCH 40/41] Change names of several arguments, change rtd. Refactor deprecation utility --- data/examples/simba.cfg | 14 ++--- docs/source/modes.rst | 2 + docs/source/simulation_parameters.rst | 16 +++-- simba/__main__.py | 12 ++-- simba/data_container.py | 5 +- simba/schedule.py | 16 ++--- simba/simulate.py | 8 +-- simba/util.py | 87 +++++++++++++++++---------- tests/test_example.py | 2 +- tests/test_schedule.py | 24 ++++---- tests/test_simulate.py | 14 ++--- tests/test_station_optimization.py | 8 +-- 12 files changed, 120 insertions(+), 88 deletions(-) diff --git a/data/examples/simba.cfg b/data/examples/simba.cfg index d0dbfa9e..a739a531 100644 --- a/data/examples/simba.cfg +++ b/data/examples/simba.cfg @@ -4,15 +4,15 @@ scenario_name = example ##### Paths ##### ### Input and output files and paths ### # Input file containing trip information (required) -input_schedule = data/examples/trips_example.csv +schedule_path = data/examples/trips_example.csv # Output files are stored here (defaults to: data/sim_outputs) # Attention: In Windows the path-length is limited to 256 characters! # Deactivate storage of output by setting output_directory = null -output_directory = data/output/ +output_path = data/output/ # Electrified stations (required) -electrified_stations = data/examples/electrified_stations.json +electrified_stations_path = data/examples/electrified_stations.json # Vehicle types (defaults to: ./data/examples/vehicle_types.json) -vehicle_types = data/examples/vehicle_types.json +vehicle_types_path = data/examples/vehicle_types.json # Path to station data with stations heights # (Optional: needed if mileage in vehicle types not constant and inclination should be considered) station_data_path = data/examples/all_stations.csv @@ -23,11 +23,11 @@ outside_temperature_over_day_path = data/examples/default_temp_winter.csv # (Optional: needed if mileage in vehicle types not constant) level_of_loading_over_day_path = data/examples/default_level_of_loading_over_day.csv # Path to configuration file for the station optimization. Only needed for mode "station_optimization" -optimizer_config = data/examples/default_optimizer.cfg +optimizer_config_path = data/examples/default_optimizer.cfg # Cost parameters (needed if cost_calculation flag is set to true, see Flag section below) -cost_parameters_file = data/examples/cost_params.json +cost_parameters_path = data/examples/cost_params.json # Path to rotation filter -rotation_filter = data/examples/rotation_filter.csv +rotation_filter_path = data/examples/rotation_filter.csv ##### Modes ##### ### Specify how you want to simulate the scenario ### diff --git a/docs/source/modes.rst b/docs/source/modes.rst index 85ed29dd..a9daedf3 100644 --- a/docs/source/modes.rst +++ b/docs/source/modes.rst @@ -212,6 +212,8 @@ To make use of this feature the parameters in the optimizer.cfg have to be set. decision_tree_path = data/last_optimization.pickle save_decision_tree = True +.. _optimizer_config: + Optimizer Configuration ################################### The functionality of the optimizer is controlled through the optimizer.cfg specified in the simba.cfg used for calling SimBA. diff --git a/docs/source/simulation_parameters.rst b/docs/source/simulation_parameters.rst index 3ce450fe..fb374332 100644 --- a/docs/source/simulation_parameters.rst +++ b/docs/source/simulation_parameters.rst @@ -32,11 +32,11 @@ The example (data/simba.cfg) contains parameter descriptions which are explained - Optional: no default given - string - scenario identifier, appended to output directory name and report file names - * - input_schedule + * - schedule_path - Mandatory: no default given - Path as string - Input file containing :ref:`schedule` information - * - Output_directory + * - output_path - Data/sim_outputs - Path as string - Output files are stored here; set to null to deactivate @@ -44,7 +44,7 @@ The example (data/simba.cfg) contains parameter descriptions which are explained - ./data/examples/vehicle_types.json - Path as string - Path to Electrified stations data - * - vehicle_types + * - vehicle_types_path - ./data/examples/vehicle_types.json - Path as string - Path to :ref:`vehicle_types` @@ -60,10 +60,18 @@ The example (data/simba.cfg) contains parameter descriptions which are explained - Optional: no default given - Path as string - Path to :ref:`level_of_loading` - * - cost_parameters_file + * - cost_parameters_path - Optional: no default given - Path as string - Path to :ref:`cost_params` + * - optimizer_config_path + - Optional: no default given + - Path as string + - Path to station optimizer config :ref:`optimizer_config` + * - rotation_filter_path + - Optional: no default given + - Path as string + - Path to rotation filter json * - mode - ['sim', 'report'] - List of modes is any order in range of ['sim', 'neg_depb_to_oppb', 'neg_oppb_to_depb', 'service_optimization', 'report'] diff --git a/simba/__main__.py b/simba/__main__.py index 619a2de4..2beb90c7 100644 --- a/simba/__main__.py +++ b/simba/__main__.py @@ -14,19 +14,19 @@ dir_name = time_str + '_' + args.scenario_name else: dir_name = time_str - if args.output_directory is not None: - args.output_directory = Path(args.output_directory) / dir_name + if args.output_path is not None: + args.output_path = Path(args.output_path) / dir_name # create subfolder for specific sim results with timestamp. # if folder doesn't exist, create folder. # needs to happen after set_options_from_config since - # args.output_directory can be overwritten by config - args.output_directory_input = args.output_directory / "input_data" + # args.output_path can be overwritten by config + args.output_directory_input = args.output_path / "input_data" try: args.output_directory_input.mkdir(parents=True, exist_ok=True) except NotADirectoryError: # can't create new directory (may be write protected): no output - args.output_directory = None - if args.output_directory is not None: + args.output_path = None + if args.output_path is not None: # copy input files to output to ensure reproducibility copy_list = [args.config, args.electrified_stations_path, args.vehicle_types_path] if "station_optimization" in args.mode: diff --git a/simba/data_container.py b/simba/data_container.py index f41fd1d9..f5869718 100644 --- a/simba/data_container.py +++ b/simba/data_container.py @@ -35,14 +35,13 @@ def __init__(self): def fill_with_args(self, args: argparse.Namespace) -> 'DataContainer': """ Fill DataContainer with data from file_paths defined in args. - :param args: Arguments containing paths for input_schedule, vehicle_types_path, + :param args: Arguments containing paths for schedule_path, vehicle_types_path, electrified_stations_path, cost_parameters_path, outside_temperature_over_day_path, level_of_loading_over_day_path, station_data_path :return: self """ - return self.fill_with_paths( - trips_file_path=args.input_schedule, + trips_file_path=args.schedule_path, vehicle_types_path=args.vehicle_types_path, electrified_stations_path=args.electrified_stations_path, cost_parameters_path=args.cost_parameters_path, diff --git a/simba/schedule.py b/simba/schedule.py index 26afc582..69e15f73 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -152,9 +152,9 @@ def from_datacontainer(cls, data: DataContainer, args): if vars(args).get("check_rotation_consistency"): # check rotation expectations inconsistent_rotations = cls.check_consistency(schedule) - if inconsistent_rotations and args.output_directory is not None: + if inconsistent_rotations and args.output_path is not None: # write errors to file - filepath = args.output_directory / "inconsistent_rotations.csv" + filepath = args.output_path / "inconsistent_rotations.csv" with open(filepath, "w", encoding='utf-8') as f: for rot_id, e in inconsistent_rotations.items(): f.write(f"Rotation {rot_id}: {e}\n") @@ -223,7 +223,7 @@ def run(self, args, mode="distributed"): For external usage the core run functionality is accessible through this function. It allows for defining a custom-made assign_vehicles method for the schedule. - :param args: used arguments are rotation_filter, path to rotation ids, + :param args: used arguments are rotation_filter_path, path to rotation ids, and rotation_filter_variable that sets mode (options: include, exclude) :type args: argparse.Namespace :param mode: SpiceEV strategy name @@ -800,7 +800,7 @@ def get_total_distance(self): def rotation_filter(self, args, rf_list=[]): """ Edits rotations according to args.rotation_filter_variable. - :param args: used arguments are rotation_filter, path to rotation ids, + :param args: used arguments are rotation_filter_path, path to rotation ids, and rotation_filter_variable that sets mode (options: include, exclude) :type args: argparse.Namespace :param rf_list: rotation filter list with strings of rotation ids (default is None) @@ -812,18 +812,18 @@ def rotation_filter(self, args, rf_list=[]): # cast rotations in filter to string rf_list = [str(i) for i in rf_list] - if args.rotation_filter is None and not rf_list: + if args.rotation_filter_path is None and not rf_list: warnings.warn("Rotation filter variable is enabled but file and list are not used.") return - if args.rotation_filter: + if args.rotation_filter_path: # read out rotations from file (one rotation ID per line) try: - with open(args.rotation_filter, encoding='utf-8') as f: + with open(args.rotation_filter_path, encoding='utf-8') as f: for line in f: rf_list.append(line.strip()) except FileNotFoundError: - warnings.warn(f"Path to rotation filter {args.rotation_filter} is invalid.") + warnings.warn(f"Path to rotation filter {args.rotation_filter_path} is invalid.") # no file, no change return # filter out rotations in self.rotations diff --git a/simba/simulate.py b/simba/simulate.py index 01139e86..c05dbafb 100644 --- a/simba/simulate.py +++ b/simba/simulate.py @@ -117,7 +117,7 @@ def modes_simulation(schedule, scenario, args): if scenario is not None and scenario.step_i > 0: # generate plot of failed scenario args.mode = args.mode[:i] + ["ABORTED"] - if args.output_directory is None: + if args.output_path is None: create_results_directory(args, i+1) if not args.skip_plots: report.generate_plots(scenario, args) @@ -260,7 +260,7 @@ def split_negative_depb(schedule, scenario, args, _i): @staticmethod def report(schedule, scenario, args, i): - if args.output_directory is None: + if args.output_path is None: return schedule, scenario # create report based on all previous modes @@ -288,12 +288,12 @@ def create_results_directory(args, i): :type i: int """ - if args.output_directory is None: + if args.output_path is None: return prior_reports = sum([m.count('report') for m in args.mode[:i]]) report_name = f"report_{prior_reports+1}" - args.results_directory = args.output_directory.joinpath(report_name) + args.results_directory = args.output_path.joinpath(report_name) args.results_directory.mkdir(parents=True, exist_ok=True) # save used modes in report version used_modes = ['sim'] + [m for m in args.mode[:i] if m not in ['sim', 'report']] diff --git a/simba/util.py b/simba/util.py index 161cba58..706350a3 100644 --- a/simba/util.py +++ b/simba/util.py @@ -262,7 +262,7 @@ def cast_float_or_none(val: any) -> any: def setup_logging(args, time_str): """ Setup logging. - :param args: command line arguments. Used: logfile, loglevel, output_directory + :param args: command line arguments. Used: logfile, loglevel, output_path :type args: argparse.Namespace :param time_str: log file name if args.logfile is not given :type time_str: str @@ -272,13 +272,14 @@ def setup_logging(args, time_str): console = logging.StreamHandler() console.setLevel(log_level) log_handlers = [console] - if args.logfile is not None and args.output_directory is not None: + + if args.logfile is not None and args.output_path is not None: # optionally to file in output dir if args.logfile: log_name = args.logfile else: log_name = f"{time_str}.log" - log_path = args.output_directory / log_name + log_path = args.output_path / log_name print(f"Writing log to {log_path}") file_logger = logging.FileHandler(log_path, encoding='utf-8') log_level_file = vars(logging).get((args.loglevel_file or args.loglevel).upper()) @@ -340,28 +341,41 @@ def get_buffer_time(trip, default=0): def replace_deprecated_arguments(args): - if args.electrified_stations is not None: - assert args.electrified_stations_path is None, \ - "Multiple electrified stations are not supported." - logging.warning("The parameter 'electrified_stations' is deprecated. " - "Use 'electrified_stations_path 'instead.") - args.electrified_stations_path = args.electrified_stations - del args.electrified_stations - - if args.vehicle_types is not None: - logging.warning("The parameter 'vehicle_types' is deprecated. " - "Use 'vehicle_types_path 'instead. The value of args.vehicle_types_path " - f"{args.vehicle_types_path} is replaced with {args.vehicle_types}.") - args.vehicle_types_path = args.vehicle_types - del args.vehicle_types - - if args.cost_parameters_file is not None: - assert args.cost_parameters_path is None, \ - "Multiple cost parameters files are not supported." - logging.warning("The parameter 'cost_parameters_file' is deprecated. " - "Use 'cost_parameters_path 'instead.") - args.cost_parameters_path = args.cost_parameters_file - del args.cost_parameters_file + # handling of args with default values + # Pairs of deprecated names and new names + deprecated_names = [ + ("input_schedule", "schedule_path"), + ("vehicle_types", "vehicle_types_path"), + ("output_directory", "output_path"), + ] + for old, new in deprecated_names: + if vars(args)[old] is not None: + logging.warning( + f"Parameter '{old}' is deprecated. Use '{new}' instead. The value of args.{new}: " + f"{args.__getattribute__(new)} is replaced with {args.__getattribute__(old)} .") + # Replace value of current name with value of deprecated name + args.__setattr__(new, args.__getattribute__(old)) + # delete deprecated name + args.__delattr__(old) + + # Pairs of deprecated names and new names and verbose name + deprecated_names = [ + ("electrified_stations", "electrified_stations_path", "electrified stations paths"), + ("cost_parameters_file", "cost_parameters_path", "costs parameter paths"), + ("rotation_filter", "rotation_filter_path", "rotation filter paths"), + ("optimizer_config", "optimizer_config_path", "station optimizer config paths"), + ] + + for old, new, description in deprecated_names: + if vars(args)[old] is not None: + assert vars(args)[new] is None, \ + f"Multiple {description} are not supported. Found values for {old} and {new}." + logging.warning(f"The parameter '{old}' is deprecated. " + f"Use '{new}' instead.") + # Replace value of current name with value of deprecated name + args.__setattr__(new, args.__getattribute__(old)) + # delete deprecated name + args.__delattr__(old) return args @@ -389,7 +403,7 @@ def get_args(): # rename special options args.timing = args.eta - mandatory_arguments = ["input_schedule", "electrified_stations_path"] + mandatory_arguments = ["schedule_path", "electrified_stations_path"] missing = [a for a in mandatory_arguments if vars(args).get(a) is None] if missing: raise Exception("The following arguments are required: {}".format(", ".join(missing))) @@ -403,9 +417,9 @@ def get_parser(): parser.add_argument('--scenario-name', help='Identifier of scenario, appended to results') # #### Paths ##### - parser.add_argument('--input-schedule', + parser.add_argument('--schedule-path', help='Path to CSV file containing all trips of schedule to be analyzed') - parser.add_argument('--output-directory', default="data/sim_outputs", + parser.add_argument('--output-path', default="data/sim_outputs", help='Location where all simulation outputs are stored') parser.add_argument('--electrified-stations-path', help='include electrified_stations json') parser.add_argument('--vehicle-types-path', default="data/examples/vehicle_types.json", @@ -421,7 +435,7 @@ def get_parser(): level of loading in case they are not in trips.csv") parser.add_argument('--cost-parameters-path', default=None, help='include cost parameters json, needed if cost_calculation==True') - parser.add_argument('--rotation-filter', default=None, + parser.add_argument('--rotation-filter-path', default=None, help='Use json data with rotation ids') # #### Modes ##### @@ -456,6 +470,9 @@ def get_parser(): parser.add_argument('--create-scenario-file', help='Write scenario.json to file') parser.add_argument('--create-trips-in-report', action='store_true', help='Write a trips.csv during report mode') + parser.add_argument('--optimizer-config-path', default=None, + help="For station_optimization an optimizer_config is needed. \ + Input a path to an .cfg file or use the default_optimizer.cfg") parser.add_argument('--rotation-filter-variable', default=None, choices=[None, 'include', 'exclude'], help='set mode for filtering schedule rotations') @@ -553,19 +570,25 @@ def get_parser(): parser.add_argument('--include-price-csv-option', '-po', metavar=('KEY', 'VALUE'), nargs=2, default=[], action='append', help='append additional argument to price signals') - parser.add_argument('--optimizer_config', default=None, - help="For station_optimization an optimizer_config is needed. \ - Input a path to an .cfg file or use the default_optimizer.cfg") parser.add_argument('--time-windows', metavar='FILE', help='use peak load windows to force lower power ' 'during times of high grid load') + # Deprecated options for downwards compatibility + parser.add_argument('--input-schedule', default=None, + help='Deprecated use "schedule-path" instead') parser.add_argument('--electrified-stations', default=None, help='Deprecated use "electrified-stations-path" instead') parser.add_argument('--vehicle-types', default=None, help='Deprecated use "vehicle-types-path" instead') parser.add_argument('--cost-parameters-file', default=None, help='Deprecated use "cost-parameters-path" instead') + parser.add_argument('--rotation-filter', default=None, + help='Deprecated use "rotation-filter-path" instead') + parser.add_argument('--output-directory', default=None, + help='Deprecated use "output-path" instead') + parser.add_argument('--optimizer_config', default=None, + help='Deprecated use "optimizer-config-path" instead') parser.add_argument('--config', help='Use config file to set arguments') return parser diff --git a/tests/test_example.py b/tests/test_example.py index 170f6340..1241beec 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -15,7 +15,7 @@ def test_example_cfg(self, tmp_path): # provide path to input data src_text = src_text.replace("data/examples", str(EXAMPLE_PATH)) # write output to tmp - src_text = re.sub(r"output_directory.+", f"output_directory = {str(tmp_path.as_posix())}", + src_text = re.sub(r"output_path.+", f"output_path = {str(tmp_path.as_posix())}", src_text) dst = tmp_path / "simba.cfg" # don't show plots. spaces are optional, so use regex diff --git a/tests/test_schedule.py b/tests/test_schedule.py index e5848f93..532cebfc 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -198,7 +198,7 @@ def test_assign_vehicles_fixed_recharge(self): args = util.get_args() args.min_recharge_deps_oppb = 1 args.min_recharge_deps_depb = 1 - args.input_schedule = file_root / "trips_assign_vehicles_extended.csv" + args.schedule_path = file_root / "trips_assign_vehicles_extended.csv" data_container = DataContainer().fill_with_args(args) generated_schedule, args = pre_simulation(args, data_container) @@ -242,7 +242,7 @@ def test_assign_vehicles_adaptive(self): """ sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] args = util.get_args() - args.input_schedule = file_root / "trips_assign_vehicles_extended.csv" + args.schedule_path = file_root / "trips_assign_vehicles_extended.csv" data_container = DataContainer().fill_with_args(args) generated_schedule, args = pre_simulation(args, data_container) @@ -295,7 +295,7 @@ def test_calculate_consumption(self, default_schedule_arguments): """ sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] args = util.get_args() - args.input_schedule = file_root / "trips_assign_vehicles.csv" + args.schedule_path = file_root / "trips_assign_vehicles.csv" data_container = DataContainer().fill_with_args(args) generated_schedule, args = pre_simulation(args, data_container) @@ -321,7 +321,7 @@ def test_get_common_stations(self, default_schedule_arguments): """ sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] args = util.get_args() - args.input_schedule = file_root / "trips_assign_vehicles.csv" + args.schedule_path = file_root / "trips_assign_vehicles.csv" data_container = DataContainer().fill_with_args(args) generated_schedule, args = pre_simulation(args, data_container) @@ -348,14 +348,14 @@ def test_get_negative_rotations(self): def test_rotation_filter(self, tmp_path): sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] args = util.get_args() - args.input_schedule = file_root / "trips_assign_vehicles.csv" + args.schedule_path = file_root / "trips_assign_vehicles.csv" data_container = DataContainer().fill_with_args(args) s, args = pre_simulation(args, data_container) args = Namespace(**{ "rotation_filter_variable": None, - "rotation_filter": None, + "rotation_filter_path": None, }) # add dummy rotations s.rotations = { @@ -370,25 +370,25 @@ def test_rotation_filter(self, tmp_path): # filtering not disabled, but neither file nor list given -> warning args.rotation_filter_variable = "include" - args.rotation_filter = None + args.rotation_filter_path = None with pytest.warns(UserWarning): s.rotation_filter(args) assert s.rotations.keys() == s.original_rotations.keys() # filter file not found -> warning - args.rotation_filter = tmp_path / "filter.txt" + args.rotation_filter_path = tmp_path / "filter.txt" with pytest.warns(UserWarning): s.rotation_filter(args) assert s.rotations.keys() == s.original_rotations.keys() # filter (include) from JSON file - args.rotation_filter.write_text("3 \n 4\n16") + args.rotation_filter_path.write_text("3 \n 4\n16") s.rotation_filter(args) assert sorted(s.rotations.keys()) == ['3', '4'] # filter (exclude) from given list args.rotation_filter_variable = "exclude" - args.rotation_filter = None + args.rotation_filter_path = None s.rotations = deepcopy(s.original_rotations) s.rotation_filter(args, rf_list=['3', '4']) assert sorted(s.rotations.keys()) == ['0', '1', '2', '5'] @@ -401,8 +401,8 @@ def test_rotation_filter(self, tmp_path): # filter nothing s.rotations = deepcopy(s.original_rotations) - args.rotation_filter = tmp_path / "filter.txt" - args.rotation_filter.write_text('') + args.rotation_filter_path = tmp_path / "filter.txt" + args.rotation_filter_path.write_text('') args.rotation_filter_variable = "exclude" s.rotation_filter(args, rf_list=[]) assert s.rotations.keys() == s.original_rotations.keys() diff --git a/tests/test_simulate.py b/tests/test_simulate.py index fb8228c2..37519c74 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -21,7 +21,7 @@ class TestSimulate: "cost_parameters_path": example_path / "cost_params.json", "outside_temperature_over_day_path": example_path / "default_temp_summer.csv", "level_of_loading_over_day_path": example_path / "default_level_of_loading_over_day.csv", - "input_schedule": example_path / "trips_example.csv", + "schedule_path": example_path / "trips_example.csv", "mode": [], "interval": 15, "propagate_mode_errors": True, @@ -34,7 +34,7 @@ class TestSimulate: "cost_parameters_path": example_path / "cost_params.json", "outside_temperature_over_day_path": example_path / "default_temp_summer.csv", "level_of_loading_over_day_path": example_path / "default_level_of_loading_over_day.csv", - "input_schedule": example_path / "trips_example.csv", + "schedule_path": example_path / "trips_example.csv", "mode": [], "min_recharge_deps_oppb": 1, "min_recharge_deps_depb": 1, @@ -144,7 +144,7 @@ def test_mode_report(self, tmp_path): args = self.get_args() args.mode = "report" args.cost_calculation = True - args.output_directory = tmp_path + args.output_path = tmp_path args.strategy_deps = "balanced" args.strategy_opps = "greedy" args.show_plots = False @@ -162,7 +162,7 @@ def test_empty_report(self, tmp_path): args.desired_soc_deps = 0 args.ALLOW_NEGATIVE_SOC = True args.cost_calculation = True - args.output_directory = tmp_path + args.output_path = tmp_path args.show_plots = False with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -171,7 +171,7 @@ def test_empty_report(self, tmp_path): def test_extended_plot(self, tmp_path): args = self.get_args() args.mode = "report" - args.output_directory = tmp_path + args.output_path = tmp_path args.show_plots = False args.extended_output_plots = True with warnings.catch_warnings(): @@ -192,7 +192,7 @@ def test_create_trips_in_report(self, tmp_path): "desired_soc_deps": 0, "ALLOW_NEGATIVE_SOC": True, "cost_calculation": False, - "output_directory": tmp_path, + "output_path": tmp_path, "show_plots": False, "create_trips_in_report": True, } @@ -204,7 +204,7 @@ def test_create_trips_in_report(self, tmp_path): simulate(Namespace(**args_dict)) # new simulation with generated trips.csv args_dict = vars(self.get_args()) - args_dict["input_schedule"] = tmp_path / "report_1/trips.csv" + args_dict["schedule_path"] = tmp_path / "report_1/trips.csv" simulate(Namespace(**(args_dict))) def test_mode_recombination(self): diff --git a/tests/test_station_optimization.py b/tests/test_station_optimization.py index 3fa822db..66a03031 100644 --- a/tests/test_station_optimization.py +++ b/tests/test_station_optimization.py @@ -115,11 +115,11 @@ def generate_datacontainer_args(self, trips_file_name="trips.csv"): sys.argv = ["foo", "--config", str(example_root / "simba.cfg")] args = util.get_args() - args.output_directory = self.tmp_path + args.output_path = self.tmp_path args.results_directory = self.tmp_path assert self.tmp_path - args.input_schedule = file_root / trips_file_name + args.schedule_path = file_root / trips_file_name data_container = DataContainer().fill_with_args(args) return data_container, args @@ -302,7 +302,7 @@ def test_deep_optimization_extended(self): sched, scen = self.generate_schedule_scenario(args, data_container) # optimization can only be properly tested if negative rotations exist assert len(sched.get_negative_rotations(scen)) > 0 - args.input_schedule = file_root / trips_file_name + args.schedule_path = file_root / trips_file_name config_path = example_root / "default_optimizer.cfg" conf = opt_util.read_config(config_path) @@ -336,7 +336,7 @@ def test_critical_stations_optimization(self, caplog): data_container.stations_data = {} args.preferred_charging_type = "oppb" sched, scen = self.generate_schedule_scenario(args, data_container) - args.input_schedule = file_root / trips_file_name + args.schedule_path = file_root / trips_file_name config_path = example_root / "default_optimizer.cfg" conf = opt_util.read_config(config_path) From 71beccd0bfa7742e38d5be616df99ef9ac714e26 Mon Sep 17 00:00:00 2001 From: "paul.scheer" Date: Tue, 24 Sep 2024 11:11:01 +0200 Subject: [PATCH 41/41] Fix path --- simba/schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simba/schedule.py b/simba/schedule.py index ce6e91e7..d20b5465 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -826,7 +826,7 @@ def rotation_filter(self, args, rf_list=[]): warnings.warn(f"Path to rotation filter {args.rotation_filter_path} is invalid.") # no file, no change return - util.save_input_file(args.rotation_filter, args) + util.save_input_file(args.rotation_filter_path, args) # filter out rotations in self.rotations if args.rotation_filter_variable == "exclude": self.rotations = {k: v for k, v in self.rotations.items() if k not in rf_list}