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 missing input files to output #208

Merged
merged 6 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
19 changes: 8 additions & 11 deletions simba/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,9 @@
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_path, args.vehicle_types_path]
if "station_optimization" in args.mode:
copy_list.append(args.optimizer_config)

# only copy cost params if they exist
if args.cost_parameters_path is not None:
copy_list.append(args.cost_parameters_path)
for c_file in map(Path, copy_list):
shutil.copy(c_file, args.output_directory_input / c_file.name)

# copy basic input to output to ensure reproducibility
util.save_input_file(args.config, args)
util.save_version(args.output_directory_input / "program_version.txt")

util.setup_logging(args, time_str)
Expand All @@ -48,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 <output_directory_name>.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)
stefansc1 marked this conversation as resolved.
Show resolved Hide resolved
logging.shutdown()
11 changes: 11 additions & 0 deletions simba/data_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def __init__(self):
# List of trip dictionaries containing trip information like arrival time and station
# departure time and station, distance and more
self.trip_data: [dict] = []
# Simulation arguments
self.args = None

def fill_with_args(self, args: argparse.Namespace) -> 'DataContainer':
""" Fill DataContainer with data from file_paths defined in args.
Expand All @@ -40,6 +42,7 @@ def fill_with_args(self, args: argparse.Namespace) -> 'DataContainer':
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,
Expand Down Expand Up @@ -69,6 +72,7 @@ def add_trip_data_from_csv(self, file_path: Path) -> 'DataContainer':
trip_d[TEMPERATURE] = util.cast_float_or_none(trip.get(TEMPERATURE))
trip_d["distance"] = float(trip["distance"])
self.trip_data.append(trip_d)
util.save_input_file(file_path, self.args)
return self

def add_station_geo_data(self, data: dict) -> None:
Expand Down Expand Up @@ -162,6 +166,7 @@ def add_station_geo_data_from_csv(self, file_path: Path) -> 'DataContainer':
msg=f"Can't parse numeric data in line {line_num + 2} from file {file_path}.",
level=100)
raise
util.save_input_file(file_path, self.args)
return self

def add_level_of_loading_data(self, data: dict) -> 'DataContainer':
Expand All @@ -186,6 +191,7 @@ def add_level_of_loading_data_from_csv(self, file_path: Path) -> 'DataContainer'
index = "hour"
column = "level_of_loading"
level_of_loading_data_dict = util.get_dict_from_csv(column, file_path, index)
util.save_input_file(file_path, self.args)
self.add_level_of_loading_data(level_of_loading_data_dict)
return self

Expand All @@ -209,6 +215,7 @@ def add_temperature_data_from_csv(self, file_path: Path) -> 'DataContainer':
index = "hour"
column = "temperature"
temperature_data_dict = util.get_dict_from_csv(column, file_path, index)
util.save_input_file(file_path, self.args)
self.add_temperature_data(temperature_data_dict)
return self

Expand All @@ -231,6 +238,7 @@ def add_cost_parameters_from_json(self, file_path: Path) -> 'DataContainer':
:return: DataContainer containing cost parameters
"""
cost_parameters = self.get_json_from_file(file_path, "cost parameters")
util.save_input_file(file_path, self.args)
self.add_cost_parameters(cost_parameters)
return self

Expand All @@ -253,6 +261,7 @@ def add_stations_from_json(self, file_path: Path) -> 'DataContainer':

"""
stations = self.get_json_from_file(file_path, "electrified stations")
util.save_input_file(file_path, self.args)
self.add_stations(stations)
return self

Expand All @@ -274,6 +283,7 @@ def add_vehicle_types_from_json(self, file_path: Path):
:return: DataContainer containing vehicle types
"""
vehicle_types = self.get_json_from_file(file_path, "vehicle types")
util.save_input_file(file_path, self.args)
self.add_vehicle_types(vehicle_types)
return self

Expand Down Expand Up @@ -311,6 +321,7 @@ def add_consumption_data_from_vehicle_type_linked_files(self):
delim = util.get_csv_delim(mileage_path)
df = pd.read_csv(mileage_path, sep=delim)
self.add_consumption_data(mileage_path, df)
util.save_input_file(mileage_path, self.args)
return self

def add_consumption_data(self, data_name, df: pd.DataFrame) -> 'DataContainer':
Expand Down
5 changes: 5 additions & 0 deletions simba/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,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}
Expand Down Expand Up @@ -867,6 +868,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():
Expand Down Expand Up @@ -993,6 +995,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,
Expand All @@ -1004,6 +1007,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:
Expand Down Expand Up @@ -1084,6 +1088,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'))
Expand Down
8 changes: 5 additions & 3 deletions simba/simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import traceback
from copy import deepcopy

from simba import report, optimization, optimizer_util
from simba import report, optimization, optimizer_util, util
from simba.data_container import DataContainer
from simba.costs import calculate_costs
from simba.optimizer_util import read_config as read_optimizer_config
Expand Down Expand Up @@ -44,6 +44,7 @@ def pre_simulation(args, data_container: DataContainer):
:return: schedule, args
:rtype: simba.schedule.Schedule, Namespace
"""

# Deepcopy args so original args do not get mutated
args = deepcopy(args)

Expand Down Expand Up @@ -202,6 +203,7 @@ def _station_optimization(schedule, scenario, args, i, single_step: bool):
conf = optimizer_util.OptimizerConfig().set_defaults()
else:
conf = read_optimizer_config(args.optimizer_config)
util.save_input_file(args.optimizer_config, args)
if single_step:
conf.early_return = True
# Work on copies of the original schedule and scenario. In case of an exception the outer
Expand All @@ -223,7 +225,7 @@ def station_optimization(schedule, scenario, args, i):
@staticmethod
def station_optimization_single_step(schedule, scenario, args, i):
""" Electrify only the station with the highest potential

:param schedule: Schedule
:type schedule: simba.schedule.Schedule
:param scenario: Scenario
Expand All @@ -232,7 +234,7 @@ def station_optimization_single_step(schedule, scenario, args, i):
:type args: argparse.Namespace
:param i: counter of modes for directory creation
:return: schedule, scenario

""" # noqa
return Mode._station_optimization(schedule, scenario, args, i, single_step=True)

Expand Down
32 changes: 32 additions & 0 deletions simba/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import csv
import json
import logging
from pathlib import Path
import shutil
import subprocess
from datetime import datetime, timedelta

Expand All @@ -18,6 +20,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 uncomment_json_file(f, char='//'):
""" Remove comments from JSON file.

Expand Down Expand Up @@ -428,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',
Expand Down
25 changes: 25 additions & 0 deletions tests/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,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))
1 change: 0 additions & 1 deletion tests/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ def test_assign_vehicles_adaptive(self):

def test_calculate_consumption(self, default_schedule_arguments):
""" Test if calling the consumption calculation works

:param default_schedule_arguments: basic arguments the schedule needs for creation
"""
sys.argv = ["foo", "--config", str(example_root / "simba.cfg")]
Expand Down
6 changes: 3 additions & 3 deletions tests/test_station_optimization.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from copy import copy, deepcopy
import json
import logging
from pathlib import Path
import pytest
import random
Expand Down Expand Up @@ -106,12 +106,12 @@ def setup_test(self, tmp_path):
dst.write_text(src_text)

def generate_datacontainer_args(self, trips_file_name="trips.csv"):
""" Check if running a basic example works and if a scenario object is returned.
""" Check if running a basic example works and return data container.

:param trips_file_name: file name of the trips file. Has to be inside the test_input_file
folder
:type trips_file_name: str
:return: schedule, scenario, args"""
:return: data_container, args"""

sys.argv = ["foo", "--config", str(example_root / "simba.cfg")]
args = util.get_args()
Expand Down
Loading