diff --git a/data/examples/simba.cfg b/data/examples/simba.cfg index a739a53..4f8e2bd 100644 --- a/data/examples/simba.cfg +++ b/data/examples/simba.cfg @@ -7,7 +7,7 @@ scenario_name = example 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 +# Deactivate storage of output by setting output_path = null output_path = data/output/ # Electrified stations (required) electrified_stations_path = data/examples/electrified_stations.json @@ -76,9 +76,10 @@ strategy_opps = greedy strategy_options_deps = {"CONCURRENCY": 1} strategy_options_opps = {} -# Cost calculation strategy -cost_calculation_strategy_deps = balanced -cost_calculation_strategy_opps = greedy +# Cost calculation to use. Remove to use default for strategy. +# Options: fixed_wo_plw, fixed_w_plw, variable_wo_plw, variable_w_plw, balanced_market, flex_window +cost_calculation_method_deps = fixed_wo_plw +cost_calculation_method_opps = fixed_wo_plw ##### Physical setup of environment ##### ### Parametrization of the physical setup ### diff --git a/docs/source/simulation_parameters.rst b/docs/source/simulation_parameters.rst index fb37433..c196601 100644 --- a/docs/source/simulation_parameters.rst +++ b/docs/source/simulation_parameters.rst @@ -104,14 +104,14 @@ The example (data/simba.cfg) contains parameter descriptions which are explained - greedy - SpiceEV Strategies (greedy, balanced, peak_shaving, peak_load_windows, balanced_market) - Charging strategy used in opportunity stations. - * - cost_calculation_strategy_deps - - strategy_deps value - - SpiceEV Strategies (greedy, balanced, peak_shaving, peak_load_windows, balanced_market) - - Strategy for cost calculation at depots. - * - cost_calculation_strategy_opps - - strategy_opps value - - SpiceEV Strategies (greedy, balanced, peak_shaving, peak_load_windows, balanced_market) - - Strategy for cost calculation at opportunity stations. + * - cost_calculation_method_deps + - fixed_wo_plw + - SpiceEV cost calculation type (fixed_wo_plw, fixed_w_plw, variable_wo_plw, variable_w_plw, balanced_market, flex_window) + - Method for cost calculation at depots. + * - cost_calculation_method_opps + - fixed_wo_plw + - SpiceEV cost calculation type, same choices as in depot + - Method for cost calculation at opportunity stations. * - preferred_charging_type - depb - depb, oppb diff --git a/simba/costs.py b/simba/costs.py index 634f369..9accbcc 100644 --- a/simba/costs.py +++ b/simba/costs.py @@ -2,11 +2,12 @@ import traceback import warnings -import spice_ev.scenario -from spice_ev.costs import calculate_costs as calc_costs_spice_ev - import simba.schedule +import spice_ev.costs +import spice_ev.scenario +import spice_ev.util + def calculate_costs(c_params, scenario, schedule, args): """ Calculates annual costs of all necessary vehicles and infrastructure. @@ -323,21 +324,23 @@ def set_electricity_costs(self): # use procurement and commodity costs read from CSV instead of SpiceEV prices, if exist prices = station.get("prices", timeseries.get("price [EUR/kWh]")) - # Get the calculation strategy / method from args. - # If no value is set, use the same strategy as the charging strategy - default_cost_strategy = vars(self.args)["strategy_" + station.get("type")] - - cost_strategy_name = "cost_calculation_strategy_" + station.get("type") - cost_calculation_strategy = (vars(self.args).get(cost_strategy_name) - or default_cost_strategy) + # Get the calculation method from args. + cost_calculation_name = "cost_calculation_method_" + station.get("type") + cost_calculation_method = vars(self.args).get(cost_calculation_name) + if cost_calculation_method is None: + # not given in config: use the same method as the charging strategy + default_strategy = vars(self.args)["strategy_" + station.get("type")] + cost_calculation_method = spice_ev.costs.DEFAULT_COST_CALCULATION[default_strategy] # calculate costs for electricity try: - if cost_calculation_strategy == "peak_load_window": - if timeseries.get("window signal [-]") is None: - raise Exception("No peak load window signal provided for cost calculation") - costs_electricity = calc_costs_spice_ev( - strategy=cost_calculation_strategy, + is_peak_load_window = cost_calculation_method.endswith("w_plw") + if is_peak_load_window and timeseries.get("window signal [-]") is None: + logging.info("Generating peak load window signal for cost calculation") + timeseries["window signal [-]"] = spice_ev.util.get_time_windows_from_json( + self.args.time_windows, gc.grid_operator, gc.voltage_level, self.scenario) + costs_electricity = spice_ev.costs.calculate_costs( + cc_type=cost_calculation_method, voltage_level=gc.voltage_level, interval=self.scenario.interval, timestamps_list=timeseries.get("time"), @@ -347,9 +350,10 @@ def set_electricity_costs(self): power_generation_feed_in_list=timeseries.get("generation feed-in [kW]"), power_v2g_feed_in_list=timeseries.get("V2G feed-in [kW]"), power_battery_feed_in_list=timeseries.get("battery feed-in [kW]"), - charging_signal_list=timeseries.get("window signal [-]"), + window_signal_list=timeseries.get("window signal [-]"), price_sheet_path=self.args.cost_parameters_path, grid_operator=gc.grid_operator, + fee_type=None, # "RLM" or "SLP" or None (based on energy consumption) power_pv_nominal=pv, ) except Exception: diff --git a/simba/rotation.py b/simba/rotation.py index 2e821af..8320625 100644 --- a/simba/rotation.py +++ b/simba/rotation.py @@ -11,6 +11,7 @@ def __init__(self, id, vehicle_type, schedule) -> None: self.trips = [] self.schedule = schedule + self.allow_opp_charging_for_oppb: bool = True self.vehicle_type = vehicle_type self.vehicle_id = None self.charging_type = None diff --git a/simba/schedule.py b/simba/schedule.py index be07e9d..7740d5b 100644 --- a/simba/schedule.py +++ b/simba/schedule.py @@ -928,8 +928,11 @@ def generate_scenario(self, args): # assume electrified station station = self.stations[gc_name] station_type = station["type"] - if station_type == 'opps' and trip.rotation.charging_type == 'depb': - # a depot bus cannot charge at an opp station + if (station_type == 'opps' and + (trip.rotation.charging_type == 'depb' or + not trip.rotation.allow_opp_charging_for_oppb)): + # a depot bus cannot charge at an opp station. + # a bus cannot charge at opps if it's not allowed station_type = None else: # get desired soc by station type and trip diff --git a/simba/util.py b/simba/util.py index 3e58f47..16afa7a 100644 --- a/simba/util.py +++ b/simba/util.py @@ -7,6 +7,7 @@ import subprocess from datetime import datetime, timedelta +from spice_ev.costs import COST_CALCULATION from spice_ev.strategy import STRATEGIES from spice_ev.util import set_options_from_config @@ -531,11 +532,11 @@ def get_parser(): parser.add_argument('--strategy-opps', default='greedy', choices=STRATEGIES, help='strategy to use at station') - # #### Cost calculation strategy ##### - parser.add_argument('--cost-calculation-strategy-deps', choices=STRATEGIES, - help='Strategy for cost calculation to use in depot') - parser.add_argument('--cost-calculation-strategy-opps', choices=STRATEGIES, - help='Strategy for cost calculation to use at station') + # #### Cost calculation method ##### + parser.add_argument('--cost-calculation-method-deps', choices=COST_CALCULATION, + help='Cost calculation to use in depot') + parser.add_argument('--cost-calculation-method-opps', choices=COST_CALCULATION, + help='Cost calculation to use at station') parser.add_argument('--strategy-options-deps', default={}, type=lambda s: s if type(s) is dict else json.loads(s), diff --git a/tests/test_cost_calculation.py b/tests/test_cost_calculation.py index 110b0b2..1413f2d 100644 --- a/tests/test_cost_calculation.py +++ b/tests/test_cost_calculation.py @@ -13,16 +13,16 @@ def test_cost_calculation(self): assert args.strategy_deps == "balanced" assert args.strategy_opps == "greedy" - args.cost_calculation_strategy_deps = None - args.cost_calculation_strategy_opps = None + args.cost_calculation_method_deps = None + args.cost_calculation_method_opps = None costs_vanilla = calculate_costs(cost_params, scenario, schedule, args) assert args.strategy_deps == "balanced" assert args.strategy_opps == "greedy" - args.cost_calculation_strategy_deps = "balanced" - args.cost_calculation_strategy_opps = "greedy" + args.cost_calculation_method_deps = "fixed_wo_plw" + args.cost_calculation_method_opps = "fixed_wo_plw" costs_with_same_strat = calculate_costs(cost_params, scenario, schedule, args) # assert all costs are the same @@ -31,8 +31,9 @@ def test_cost_calculation(self): assert (costs_vanilla.costs_per_gc[station][key] == costs_with_same_strat.costs_per_gc[station][key]), station - args.cost_calculation_strategy_opps = "balanced_market" - args.cost_calculation_strategy_deps = "balanced_market" + # test with different method (balanced_market costs for balanced strategy) + args.cost_calculation_method_deps = "balanced_market" + args.cost_calculation_method_opps = "balanced_market" costs_with_other_strat = calculate_costs(cost_params, scenario, schedule, args) print(costs_vanilla.costs_per_gc["cumulated"]["c_total_annual"]) print(costs_with_other_strat.costs_per_gc["cumulated"]["c_total_annual"]) @@ -43,21 +44,15 @@ def test_cost_calculation(self): assert (costs_vanilla.costs_per_gc[station][key] != costs_with_other_strat.costs_per_gc[station][key]), key - args.cost_calculation_strategy_opps = "peak_load_window" - args.cost_calculation_strategy_deps = "peak_load_window" + # PLW: will create window time series before cost calculation + args.cost_calculation_method_deps = "fixed_w_plw" + args.cost_calculation_method_opps = "fixed_w_plw" costs_with_other_strat = calculate_costs(cost_params, scenario, schedule, args) + """ station = "cumulated" for key in costs_vanilla.costs_per_gc[station]: if "el_energy" not in key: continue assert (costs_vanilla.costs_per_gc[station][key] != - costs_with_other_strat.costs_per_gc[station][key]), key - - args.cost_calculation_strategy_opps = "peak_shaving" - args.cost_calculation_strategy_deps = "peak_shaving" - costs_with_other_strat = calculate_costs(cost_params, scenario, schedule, args) - # assert all costs are the same - for station in costs_vanilla.costs_per_gc: - for key in costs_vanilla.costs_per_gc[station]: - assert (costs_vanilla.costs_per_gc[station][key] == - costs_with_other_strat.costs_per_gc[station][key]), station + costs_with_other_strat.costs_per_gc[station][key]), key + """ diff --git a/tests/test_schedule.py b/tests/test_schedule.py index f362135..b2f1168 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -60,6 +60,32 @@ def basic_run(self): class TestSchedule(BasicSchedule): + def test_allow_opp_charging(self): + # test if the schedule properly skips charging events if the rotation is not allowed + # to opportunity charge + sched, scen, args = BasicSchedule().basic_run() + oppb_rotations = [rot for rot in sched.rotations.values() if rot.charging_type == "oppb"] + assert len(oppb_rotations) >= 1 + oppb_rotation = oppb_rotations[0] + vehicle = oppb_rotation.vehicle_id + assert oppb_rotation.allow_opp_charging_for_oppb is True + index = list(scen.components.vehicles.keys()).index(vehicle) + min_soc = min(s[index] for s in scen.socs if s[index] is not None) + vehicle_event = [e for e in scen.events.vehicle_events if e.vehicle_id == vehicle] + charge_events = 0 + for e in vehicle_event: + if vars(e).get("update", {}).get("connected_charging_station") is not None: + charge_events += 1 + + assert charge_events > 0, \ + "Rotation has no charge events to check if allow_opp_charging_for_oppb works" + for rot in oppb_rotations: + rot.allow_opp_charging_for_oppb = False + scen2 = sched.run(args) + + index = list(scen2.components.vehicles.keys()).index(vehicle) + min_soc_charging_not_allowed = min(s[index] for s in scen2.socs if s[index] is not None) + assert min_soc_charging_not_allowed < min_soc def test_optional_timeseries(self): # Test if simulation runs if level of loading and temperature timeseries is not given