Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add load_pickle mode and create_pickle_in_report option #205

Merged
merged 8 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions data/examples/minimal.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Input and output files and paths ###
# Input file containing trip information
schedule_path = data/examples/trips_example.csv
# Electrified stations
electrified_stations_path = data/examples/electrified_stations.json
# Vehicle types
# Not strictly needed (defaults to: ./data/examples/vehicle_types.json),
# but if vehicle types have constant mileage, no other mileage files are needed
vehicle_types_path = data/examples/vehicle_types_constant_mileage.json
3 changes: 3 additions & 0 deletions data/examples/minimal_pickle.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mode = ["load_pickle", "report"]
# Load this pickle file, expects load_pickle as first mode
load_pickle = scenario.pkl
7 changes: 6 additions & 1 deletion data/examples/simba.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ rotation_filter_path = data/examples/rotation_filter.csv
# recombination splits and merges rotations to use the optimal number of busses
# report generates simulation output files, including costs.
mode = ["sim", "report"]
#mode = ["sim", "neg_depb_to_oppb", "service_optimization", "station_optimization", "remove_negative", "split_negative_depb", "report"]
#mode = ["sim", "load_pickle", "neg_depb_to_oppb", "service_optimization", "station_optimization", "remove_negative", "split_negative_depb", "report"]

##### Flags #####
### Activate optional functions ###
Expand All @@ -63,6 +63,11 @@ extended_output_plots = false
rotation_filter_variable = null
# Write a new trips.csv during report mode to output directory? (default: false)
create_trips_in_report = false
# Pickle current schedule and scenario during report mode
# create_pickle_in_report = false
# Load this pickle file, expects load_pickle as first mode
# load_pickle = "example.pkl"
stefansc1 marked this conversation as resolved.
Show resolved Hide resolved


##### Charging strategy #####
# Preferred charging type. Options: depb, oppb (default: depb)
Expand Down
43 changes: 43 additions & 0 deletions data/examples/vehicle_types_constant_mileage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"AB": { // vehicle_type
"depb": { // charging_type
"name": "articulated bus - depot charging", // long name
"capacity": 250, // battery capacity in kWh
"charging_curve": [[0, 150], [0.8, 150], [1, 15]], // charging curve [SoC, kW]
"min_charging_power": 0, // min charging power in KW
"v2g": false, // Is vehicle capable of vehicle to grid?
"mileage": 1.0, // mileage in kWh/km or link to consumption.csv
"battery_efficiency": 0.95, // optional. default: 0.95
"idle_consumption" : 0.0 // [kWh/h] consumption while standing during a rotation
},
"oppb": {
"name": "articulated bus - opportunity charging",
"capacity": 150,
"charging_curve": [[0, 250], [0.8, 250], [1, 25]],
"min_charging_power": 0,
"v2g": false,
"mileage": 1.0,
"idle_consumption" : 0.0
}
},
"SB": {
"depb": {
"name": "solo bus - depot charging",
"capacity": 250,
"charging_curve": [[0, 150], [0.8, 150], [1, 15]],
"min_charging_power": 0,
"v2g": false,
"mileage": 1.2,
"idle_consumption" : 0.0
},
"oppb": {
"name": "solo bus - opportunity charging",
"capacity": 150,
"charging_curve": [[0, 250], [0.8, 250], [1, 25]],
"min_charging_power": 0,
"v2g": false,
"mileage": 1.1,
"idle_consumption" : 0.0
}
}
}
9 changes: 9 additions & 0 deletions docs/source/modes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ Simple Simulation

The simple simulation case is the default mode. Its usage is explained in :ref:`Getting Started`. Every chain of modes starts with a simple simulation, even if it is not explicitly listed in the modes. The simulation takes the scenario as is. No parameters will be adjusted, optimized or changed in any way. The charging type for each vehicle is read from the rotation information from the trips.csv if this data is included. If the data is not included *preferred_charging_type* from the config file is used, as long as the provided vehicles data provides the preferred_charging_type for the specified vehicle type.

Load Pickle
-----------

::

mode = ["load_pickle"]

Instead of simulating given scenario, read in pickle file instead. Needs the `load_pickle` option to point to a pickle file. If the `load_pickle` option is set, `load_pickle` must be the first mode. However, this mode may come in later in the mode chain again, reloading schedule and scenario from file. Options in config file that are stored in schedule are ignored.
stefansc1 marked this conversation as resolved.
Show resolved Hide resolved

.. _neg_depb_to_oppb:

Negative Depot to Opportunity Charger
Expand Down
16 changes: 16 additions & 0 deletions docs/source/simulation_parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ 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
* - rotation_filter_variable
- null
- string
- How to filter rotations according to file 'rotation_filter': options are "include" (whitelist), "exclude" (blacklist), null (ignore)
* - create_trips_in_report
- false
- Boolean
- Write a new trips.csv during report mode to output directory?
* - create_pickle_in_report
- false
- Boolean
- Pickle current schedule and scenario during report mode
* - load_pickle
- Optional, no default given
- Path to pickle file
- Load schedule and scenario from this pickle file, expects load_pickle as first mode
* - extended_output_plots
- false
- Boolean
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ numpy >= 1.12.0
matplotlib
-e git+https://github.com/rl-institut/spice_ev.git@dev#egg=spice_ev
pandas
dill
9 changes: 9 additions & 0 deletions simba/report.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Module to generate meaningful output files and/or figures to describe simulation process. """
import csv
import datetime
import dill as pickle
import logging
from math import ceil
import re
Expand Down Expand Up @@ -347,6 +348,14 @@ def generate(schedule, scenario, args):
file_path = args.results_directory / "trips.csv"
write_csv(generate_trips_timeseries_data(schedule), file_path)

if vars(args).get('create_pickle_in_report', False):
file_path = args.results_directory / "scenario.pkl"
with file_path.open('wb') as f:
pickle.dump({
"schedule": schedule,
"scenario": scenario,
}, f)

# summary of used vehicle types and all costs
if args.cost_calculation:
file_path = args.results_directory / "summary_vehicles_costs.csv"
Expand Down
38 changes: 31 additions & 7 deletions simba/simulate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import dill as pickle
import traceback
from copy import deepcopy

Expand All @@ -22,13 +23,23 @@ def simulate(args):
:return: final schedule and scenario
:rtype: tuple
"""
# The DataContainer stores various input data.
data_container = DataContainer().fill_with_args(args)

schedule, args = pre_simulation(args, data_container)
scenario = schedule.run(args)
schedule, scenario = modes_simulation(schedule, scenario, args)
return schedule, scenario
if vars(args).get("load_pickle"):
# load pickle file: skip pre_simulation
if isinstance(args.mode, list):
first_mode = args.mode[0]
else:
first_mode = args.mode
stefansc1 marked this conversation as resolved.
Show resolved Hide resolved
assert first_mode == "load_pickle", "Load pickle: first mode must be load_pickle"
# schedule and scenario read out from pickle file in first mode
# DataContainer is part of schedule
schedule = None
scenario = "pickle" # must not be None
stefansc1 marked this conversation as resolved.
Show resolved Hide resolved
else:
# DataContainer stores various input data
data_container = DataContainer().fill_with_args(args)
schedule, args = pre_simulation(args, data_container)
scenario = schedule.run(args)
return modes_simulation(schedule, scenario, args)


def pre_simulation(args, data_container: DataContainer):
Expand Down Expand Up @@ -260,6 +271,19 @@ def split_negative_depb(schedule, scenario, args, _i):
scenario = recombined_schedule.run(args)
return recombined_schedule, scenario

@staticmethod
def load_pickle(_schedule=None, _scenario=None, args=None, _i=None):
with open(args.load_pickle, 'rb') as f:
unpickle = pickle.load(f)
schedule = unpickle["schedule"]
scenario = unpickle["scenario"]
# DataContainer is part of schedule
# However, cost parameters are supposed to be mutable after loading from pickle
if args.cost_parameters_path:
schedule.data_container.add_cost_parameters_from_json(args.cost_parameters_path)

return schedule, scenario

@staticmethod
def report(schedule, scenario, args, i):
if args.output_path is None:
Expand Down
20 changes: 15 additions & 5 deletions simba/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,10 +446,14 @@ def get_args():
# rename special options
args.timing = args.eta

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)))
# check mandatory arguments
if not vars(args).get("load_pickle"):
mandatory_arguments = ["schedule_path", "electrified_stations_path"]
if "load_pickle" in args.mode:
mandatory_arguments.append("load_pickle")
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)))

return args

Expand Down Expand Up @@ -483,12 +487,13 @@ def get_parser():

# #### Modes #####
mode_choices = [
'sim', 'neg_depb_to_oppb', 'neg_oppb_to_depb', 'service_optimization',
'sim', 'load_pickle', 'neg_depb_to_oppb', 'neg_oppb_to_depb', 'service_optimization',
'station_optimization', 'remove_negative', 'split_negative_depb', 'report']
parser.add_argument('--mode', default=['sim', 'report'], nargs='*', choices=mode_choices,
help=f"Specify what you want to do. Choose one or more from \
{', '.join(mode_choices)}. \
sim runs a single simulation with the given inputs. \
load_pickle loads files specified in load_pickle argument. \
neg_depb_to_oppb changes charging type of negative depb rotations. \
neg_oppb_to_depb changes charging type of negative oppb rotations. \
service optimization finds the largest set of electrified rotations. \
Expand All @@ -513,6 +518,11 @@ 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('--create-pickle-in-report', action='store_true',
help='Pickle current schedule and scenario during report mode')
parser.add_argument('--load-pickle',
help='Load given pickle file, expects load_pickle as first 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")
Expand Down
59 changes: 40 additions & 19 deletions tests/test_simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def get_args(self):
parser.set_defaults(**self.NON_DEFAULT_VALUES)
# get all args with default values
args, _ = parser.parse_known_args()
util.mutate_args_for_spiceev(args)
args = util.replace_deprecated_arguments(args)

return args
Expand Down Expand Up @@ -120,7 +121,6 @@ def test_mode_service_opt(self):
# all rotations remain negative
args.desired_soc_deps = 0
args.desired_soc_opps = 0
args.ALLOW_NEGATIVE_SOC = True
simulate(args)

def test_mode_change_charge_type(self):
Expand All @@ -129,14 +129,12 @@ def test_mode_change_charge_type(self):
args.mode = "neg_oppb_to_depb"
args.desired_soc_deps = 0
args.desired_soc_opps = 0
args.ALLOW_NEGATIVE_SOC = True
simulate(args)

def test_mode_remove_negative(self):
args = self.get_args()
args.mode = "remove_negative"
args.desired_soc_deps = 0
args.ALLOW_NEGATIVE_SOC = True
simulate(args)

def test_mode_report(self, tmp_path):
Expand All @@ -160,7 +158,6 @@ def test_empty_report(self, tmp_path):
args = self.get_args()
args.mode = ["remove_negative", "report"]
args.desired_soc_deps = 0
args.ALLOW_NEGATIVE_SOC = True
args.cost_calculation = True
args.output_path = tmp_path
args.show_plots = False
Expand All @@ -186,26 +183,50 @@ def test_extended_plot(self, tmp_path):

def test_create_trips_in_report(self, tmp_path):
# create_trips_in_report option: must generate valid input trips.csv
args_dict = vars(self.get_args())
update_dict = {
"mode": ["report"],
"desired_soc_deps": 0,
"ALLOW_NEGATIVE_SOC": True,
"cost_calculation": False,
"output_path": tmp_path,
"show_plots": False,
"create_trips_in_report": True,
}
args_dict.update(update_dict)
args = self.get_args()
args.mode = "report"
args.desired_soc_deps = 0
args.cost_calculation = False
args.output_path = tmp_path
args.show_plots = False
args.create_trips_in_report = True

# simulate base scenario, report generates new trips.csv in (tmp) output
with warnings.catch_warnings():
warnings.simplefilter("ignore")
simulate(Namespace(**args_dict))
simulate(args)
# new simulation with generated trips.csv
args_dict = vars(self.get_args())
args_dict["schedule_path"] = tmp_path / "report_1/trips.csv"
simulate(Namespace(**(args_dict)))
args = self.get_args()
args.input_schedule = tmp_path / "report_1/trips.csv"
simulate(args)

def test_pickle(self, tmp_path):
# create pickle in report
args = self.get_args()
args.mode = "report"
args.show_plots = False
args.cost_calculation = False
args.output_path = tmp_path
args.create_pickle_in_report = True
simulate(args)
pickle_path = tmp_path / "report_1/scenario.pkl"
assert pickle_path.exists()

# read in pickle for new simulation
args.mode = "load_pickle"
args.output_path = None
args.load_pickle = pickle_path
args.cost_parameters_path = None # keep original cost parameters
schedule, _ = simulate(args)
assert "foo" not in schedule.data_container.cost_parameters_data

# replace cost parameters after loading pickle
args.mode = ["load_pickle"]
args.cost_parameters_path = tmp_path / "cost_params.json"
with open(args.cost_parameters_path, "w") as f:
f.write('{"foo": 1}')
schedule, _ = simulate(args)
assert schedule.data_container.cost_parameters_data["foo"] == 1

def test_mode_recombination(self):
args = self.get_args()
Expand Down
Loading