Skip to content

Commit 69b0810

Browse files
authored
Merge pull request #221 from rl-institut/feature/battery_strategy
battery strategies
2 parents 868b529 + 10dc573 commit 69b0810

File tree

11 files changed

+279
-141
lines changed

11 files changed

+279
-141
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/__pycache__
33
*.pyc
44
.DS_Store
5+
spice_ev.egg-info/
56

67
.coverage
78
htmlcov/

doc/source/charging_strategies_incentives.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,23 @@ again, the process is repeated.
112112
.. image:: _files/example_strategies.png
113113
:width: 80 %
114114

115+
Battery Strategies
116+
==================
117+
118+
Stationary batteries may be managed through a special battery strategy, instead of relying on a charging strategy's behavior. The following battery strategies are supported:
119+
120+
surplus
121+
-------
122+
Charge stationary batteries if there is a surplus of energy, for example through feed-in from photovoltaics. Discharge to support a GC in times of high load (specifically, if the maximum power at a GC would be exceeded).
123+
124+
peak_shaving
125+
------------
126+
Charge stationary batteries in times of low load (maximum power of a GC not yet reached, includes surplus) and discharge in times of high load (maximum power of a GC exceeded).
127+
128+
peak_load_window
129+
----------------
130+
Similar to peak_shaving, but only charge from GC outside of peak load windows. May use surplus inside of peak load windows. Discharge to support GC as needed.
131+
115132
Cost calculation
116133
================
117134

doc/source/simulating_with_spiceev.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ stationary batteries or V2G, you need to set the target SOC parameter of the bat
209209
+---------------------+----------------------------+---------------------------------------------------------+-------------+--------------+---------------------+--------------+------------------+---------------------+------------------+-----------------+
210210
| **Strategy option** | **Default** | **Explanation** | **Greedy** | **Balanced** | **Balanced Market** | **Schedule** | **Peak Shaving** | **Peak Load Window**| **Flex Window** | **Distributed** |
211211
+---------------------+----------------------------+---------------------------------------------------------+-------------+--------------+---------------------+--------------+------------------+---------------------+------------------+-----------------+
212-
| Perfect_Foresight | True | All events and loads are known at start of simulation. | | | | | x | | | |
212+
| battery_strategy | None | Use special strategy for stationary batteries. | x | x | x | | x | x | x | x |
213+
+---------------------+----------------------------+---------------------------------------------------------+-------------+--------------+---------------------+--------------+------------------+---------------------+------------------+-----------------+
214+
| perfect_foresight | True | All events and loads are known at start of simulation. | | | | | x | | | |
213215
+---------------------+----------------------------+---------------------------------------------------------+-------------+--------------+---------------------+--------------+------------------+---------------------+------------------+-----------------+
214216
| CONCURRENCY | 1.0 | Reduce maximum available power at each charging station.| x | | x | | | | x | |
215217
| | | | | | | | | | | |

spice_ev/generate/generate_schedule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def clamp_to_gc(power):
9999
for step_i in range(scenario.n_intervals):
100100
with warnings.catch_warnings():
101101
warnings.simplefilter('ignore', UserWarning)
102-
s.step(event_steps[step_i])
102+
s.pre_step(event_steps[step_i])
103103

104104
current_datetime = scenario.start_time + scenario.interval * step_i
105105
currently_in_core_standing_time = \

spice_ev/scenario.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def run(self, strategy_name, options):
125125

126126
# process events
127127
try:
128-
super(type(strat), strat).step(event_steps[step_i])
128+
strat.pre_step(event_steps[step_i])
129129
except Exception:
130130
error = traceback.format_exc()
131131

@@ -183,6 +183,7 @@ def run(self, strategy_name, options):
183183
try:
184184
if error is None:
185185
res = strat.step()
186+
strat.post_step()
186187
except Exception:
187188
# error during strategy: add dummy result and abort
188189
error = traceback.format_exc() if error is None else error

spice_ev/strategies/distributed.py

Lines changed: 14 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -55,32 +55,12 @@ def __init__(self, comps, start_time, **kwargs):
5555

5656
# prepare batteries
5757
for b_id, bat in self.world_state.batteries.items():
58-
# make note to run GC even if no vehicles are connected
58+
# create look-up-table for GC ID -> battery dict
5959
if self.gc_battery.get(bat.parent):
6060
self.gc_battery[bat.parent][b_id] = bat
6161
else:
6262
self.gc_battery[bat.parent] = {b_id: bat}
6363

64-
station_type = self.strategies.get(bat.parent)
65-
if station_type is None or station_type[0] == "deps":
66-
# only batteries at opportunity stations need preparation
67-
continue
68-
name = f"stationary_{b_id}"
69-
# create new vehicle type equivalent to battery (only charging, no discharging)
70-
self.virtual_vt[name] = components.VehicleType({
71-
"name": name,
72-
"capacity": bat.capacity,
73-
"charging_curve": bat.charging_curve.points,
74-
"min_charging_power": bat.min_charging_power,
75-
"battery_efficiency": bat.efficiency,
76-
})
77-
# set up virtual charging station
78-
self.virtual_cs[name] = components.ChargingStation({
79-
"parent": bat.parent,
80-
"max_power": bat.charging_curve.max_power,
81-
"min_power": bat.min_charging_power,
82-
})
83-
8464
def step(self):
8565
""" Calculates charging power in each timestep.
8666
@@ -189,6 +169,7 @@ def step(self):
189169
# copy reference of current GC and relevant vehicles
190170
# changes during simulation reflect back to original!
191171
new_world_state.grid_connectors = {gc_id: gc}
172+
strat.gc_power[gc_id] = gc.cur_max_power
192173

193174
# filter future events for this GC (within event horizon)
194175
new_world_state.future_events = []
@@ -221,71 +202,25 @@ def step(self):
221202
new_world_state.future_events.append(event)
222203

223204
# stationary batteries
224-
avail_bat_power = dict()
225-
if station_type == "deps":
226-
# depot: use stationary batteries according to selected strategy
227-
new_world_state.batteries = self.gc_battery.get(gc_id, {})
228-
else:
229-
# opportunity station:
230-
# - charge according to strategy (simulate by creating equivalent vehicle)
231-
# - discharge when needed power is above GC max power
232-
for b_id, battery in self.gc_battery.get(gc_id, {}).items():
233-
if connected_vehicles:
234-
# vehicle present: support GC (increase GC max power)
235-
power = battery.get_available_power(self.interval)
236-
if power < battery.min_charging_power:
237-
# below minimum (dis)charging power
238-
continue
239-
total_time = self.interval.total_seconds() / 3600
240-
energy_delta = power / battery.efficiency * total_time
241-
soc_delta = energy_delta / battery.capacity
242-
if soc_delta < self.EPS:
243-
# remaining power too small
244-
continue
245-
avail_bat_power[b_id] = (power, gc.cur_max_power)
246-
gc.cur_max_power += power
247-
else:
248-
# vacant station: charge with strategy until vehicle arrives
249-
name = f"stationary_{b_id}"
250-
arrive = next_arrival.get(gc_id, self.current_time+self.ARRIVAL_HORIZON)
251-
bat_vehicle = components.Vehicle({
252-
"vehicle_type": name,
253-
"connected_charging_station": name,
254-
"soc": battery.soc,
255-
"desired_soc": 1,
256-
"estimated_time_of_departure": str(arrive),
257-
}, self.virtual_vt)
258-
new_world_state.vehicle_types[name] = self.virtual_vt[name]
259-
new_world_state.charging_stations[name] = self.virtual_cs[name]
260-
new_world_state.vehicles[b_id] = bat_vehicle
205+
gc_batteries = self.gc_battery.get(gc_id, dict())
206+
new_world_state.batteries = gc_batteries
207+
if strat.battery_strategy is not None:
208+
# special stationary battery strategy: ignore charging strategy
209+
# remove batteries from GC, so they can't charge
210+
# add available battery power to GC power
211+
for bat in gc_batteries.values():
212+
available_power = bat.get_available_power(self.interval)
213+
gc.cur_max_power += available_power
214+
bat.parent = None
261215

262216
# update world state of strategy
263217
strat.current_time = self.current_time
264218
strat.world_state = new_world_state
265219
# run sub-strategy
266220
commands = strat.step()["commands"]
267-
# update stationary batteries
268-
if station_type == "opps":
269-
for b_id, battery in self.gc_battery.get(gc_id, {}).items():
270-
power = avail_bat_power.get(b_id)
271-
if power is not None:
272-
# battery used to support GC -> revert max_power, discharge
273-
gc.cur_max_power = power[1]
274-
power_needed = gc.get_current_load() - gc.cur_max_power
275-
power = battery.unload(self.interval, target_power=max(power_needed, 0))
276-
gc.add_load(b_id, -power['avg_power'])
277-
continue
278-
name = f"stationary_{b_id}"
279-
if name in commands:
280-
# battery is simulated as vehicle -> apply changes
281-
# remove from commands
282-
del commands[name]
283-
# and add as battery
284-
# this will crash if virtual CS power has not been added correctly to GC
285-
gc.add_load(b_id, gc.current_loads.pop(name))
286-
# update battery SoC
287-
battery.soc = strat.world_state.vehicles[b_id].battery.soc
288221
charging_stations.update(commands)
222+
# update batteries according to battery strategy
223+
strat.post_step()
289224

290225
# all vehicles charged
291226
charging_stations.update(self.distribute_surplus_power())

spice_ev/strategies/peak_load_window.py

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from copy import deepcopy
22
import datetime
3-
import json
43
import warnings
54

65
from spice_ev import events, util
@@ -23,54 +22,9 @@ def __init__(self, components, start_time, **kwargs):
2322

2423
if self.time_windows is None:
2524
raise Exception("Need time windows for Peak Load Window strategy")
26-
with open(self.time_windows, 'r') as f:
27-
self.time_windows = json.load(f)
28-
29-
# check time windows
30-
# start year in time windows?
31-
years = set()
32-
for grid_operator in self.time_windows.values():
33-
for window in grid_operator.values():
34-
years.add(int(window["start"][:4]))
35-
assert len(years) > 0, "No time windows given"
36-
# has the scenario year to be replaced because it is not in time windows?
37-
replace_year = start_time.year not in years
38-
if replace_year:
39-
replace_year = start_time.year
40-
old_year = sorted(years)[0]
41-
warnings.warn("Time windows do not include scenario year,"
42-
f"replacing {old_year} with {replace_year}")
43-
# cast strings to dates/times, maybe replacing year
44-
grid_operator = None
45-
for grid_operator, grid_operator_seasons in self.time_windows.items():
46-
for season, info in grid_operator_seasons.items():
47-
start_date = datetime.date.fromisoformat(info["start"])
48-
if replace_year and start_date.year == old_year:
49-
start_date = start_date.replace(year=replace_year)
50-
info["start"] = start_date
51-
end_date = datetime.date.fromisoformat(info["end"])
52-
if replace_year and end_date.year == old_year:
53-
end_date = end_date.replace(year=replace_year)
54-
info["end"] = end_date
55-
for level, windows in info.get("windows", {}).items():
56-
# cast times to datetime.time, store as tuples
57-
info["windows"][level] = [
58-
(datetime.time.fromisoformat(t[0]), datetime.time.fromisoformat(t[1]))
59-
for t in windows]
60-
self.time_windows[grid_operator][season] = info
61-
62-
gcs = self.world_state.grid_connectors
63-
64-
for gc_id, gc in gcs.items():
65-
if gc.voltage_level is None:
66-
warnings.warn(f"GC {gc_id} has no voltage level, might not find time window")
67-
warnings.warn("SETTING VOLTAGE LEVEL TO MV")
68-
gc.voltage_level = "MV" # TODO remove
69-
if gc.grid_operator is None:
70-
warnings.warn(f"GC {gc_id} has no grid operator, might not find time window")
71-
# take the first grid operator from time windows
72-
warnings.warn(f"SETTING GRID OPERATOR TO {grid_operator}")
73-
gc.grid_operator = grid_operator # TODO remove
25+
if type(self.time_windows) is not dict:
26+
# parse time windows file at path into time windows dict
27+
util.parse_time_windows(self, self.time_windows, start_time=start_time, check_gc=True)
7428

7529
# perfect foresight for grid and local load events
7630
local_events = [e for e in self.events.grid_operator_signals
@@ -97,6 +51,7 @@ def __init__(self, components, start_time, **kwargs):
9751
elif event.event_type == "departure":
9852
stop_time = max(stop_time, event.start_time)
9953

54+
gcs = self.world_state.grid_connectors
10055
# restructure events (like event_steps): list with events for each timestep
10156
# also, find highest peak of GC power within time windows
10257
self.events = []

0 commit comments

Comments
 (0)