Skip to content

Commit

Permalink
Merge pull request #211 from rl-institut/bugfix/optional_timeseries_a…
Browse files Browse the repository at this point in the history
…nd_compatibility

Make lol and temperatures optional. make cfg parameter backwards compatible
  • Loading branch information
j-brendel authored Sep 24, 2024
2 parents 3531dc9 + 71beccd commit 7b4bbda
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 71 deletions.
8 changes: 4 additions & 4 deletions data/examples/simba.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ 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_path = data/examples/electrified_stations.json
# Vehicle types (defaults to: ./data/examples/vehicle_types.json)
Expand All @@ -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_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 ###
Expand Down
2 changes: 2 additions & 0 deletions docs/source/modes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 12 additions & 4 deletions docs/source/simulation_parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ 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
* - electrified_stations_path
- ./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`
Expand All @@ -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']
Expand Down
8 changes: 4 additions & 4 deletions simba/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
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:
Expand Down
4 changes: 2 additions & 2 deletions simba/data_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ 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
"""
self.args = args

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,
Expand Down
46 changes: 27 additions & 19 deletions simba/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,38 @@ 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
trip["height_difference"] = schedule.get_height_difference(
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({
Expand All @@ -143,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")
Expand Down Expand Up @@ -214,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
Expand Down Expand Up @@ -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"]
Expand All @@ -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):
Expand Down Expand Up @@ -792,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)
Expand All @@ -804,21 +812,21 @@ 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
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}
Expand Down
8 changes: 4 additions & 4 deletions simba/simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,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)
Expand Down Expand Up @@ -262,7 +262,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
Expand Down Expand Up @@ -290,12 +290,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']]
Expand Down
Loading

0 comments on commit 7b4bbda

Please sign in to comment.