-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsessions.py
356 lines (305 loc) · 13.5 KB
/
sessions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import json
from uuid import uuid4, UUID
import hashlib
import pandas as pd
import oemof.solph as solph
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
from stemp_abw.models import Scenario, RepoweringScenario, ScenarioData
from stemp_abw.app_settings import CONTROL_VALUES_MAP
from stemp_abw.simulation.bookkeeping import simulate_energysystem
from stemp_abw.app_settings import SIMULATION_CFG as SIM_CFG
from stemp_abw.simulation.esys import create_nodes
from stemp_abw.results.results import Results
from stemp_abw.results.io import oemof_json_to_results
class UserSession(object):
"""User session
Attributes
----------
user_scenario : :class:`stemp_abw.models.Scenario`
User's scenario (data updated continuously during tool operation)
simulation : :class:`stemp_abw.sessions.Simulation`
Holds data related to energy system
mun_to_reg_ratios : :obj:`dict`
Capacity ratios of municipality to regional values, for details see
:meth:`stemp_abw.sessions.UserSession.create_mun_data_ratio_for_aggregation`
tech_ratios : :pandas:`pandas.DataFrame`
Capacity ratios of specific technologies in the region belonging to the
same category from status quo scenario, for details see
:meth:`stemp_abw.sessions.UserSession.create_reg_tech_ratios`
tracker : :class:`stemp_abw.sessions.Tracker`
Holds tool usage data
Notes
-----
INSERT NOTES
"""
def __init__(self):
self.user_scenario = self.__scenario_to_user_scenario()
self.simulation = Simulation(session=self)
self.mun_to_reg_ratios = self.create_mun_data_ratio_for_aggregation()
self.tech_ratios = self.create_reg_tech_ratios()
self.tracker = Tracker(session=self)
self.highcharts_temp = None
@property
def scenarios(self):
"""Return all default scenarios (not created by user)"""
return {scn.id: scn
for scn in Scenario.objects.filter(
is_user_scenario=False).all()
}
@property
def region_data(self):
"""Aggregate municipal data and return region data for user scenario
Notes
-----
Also includes regional params contained in scenario.
"""
return self.region_data_for_scenario(self.user_scenario)
def region_data_for_scenario(self, scenario):
"""Aggregate municipal data and return region data for given scenario
Notes
-----
Also includes regional params contained in scenario.
"""
scn_data = json.loads(scenario.data.data)
reg_data = pd.DataFrame.from_dict(scn_data['mun_data'],
orient='index'). \
sum(axis=0).round(decimals=3).to_dict()
reg_data.update(scn_data['reg_params'])
return reg_data
def __scenario_to_user_scenario(self, scn_id=None):
"""Make a copy of a scenario and return as user scenario
At startup, use status quo scenario as user scenario. This may change
when a different scenario is applied in the tool (apply button).
Parameters
----------
scn_id : obj:`int`
id of scenario. If not provided, status quo scenario from DB is used
"""
if scn_id is None:
# TODO: Use exists() instead
try:
scn = Scenario.objects.get(name='Status quo')
except ObjectDoesNotExist:
raise ObjectDoesNotExist('Szenario "Status quo" nicht gefunden!')
else:
scn = self.scenarios[int(scn_id)]
scn.name = 'User Scenario {uuid}'.format(uuid=str(uuid4()))
scn.description = ''
scn.id = None
scn.is_user_scenario = True
scn.created = timezone.now()
# TODO: Save scenario
# scn.save()
return scn
def set_user_scenario(self, scn_id):
"""Set user scenario to scenario from DB
Parameters
----------
scn_id : :obj:`int`
id of scenario
"""
self.user_scenario = self.__scenario_to_user_scenario(scn_id=scn_id)
def get_control_values(self, scenario):
"""Return a JSON with values for the UI controls (e,g, sliders)
for a given scenario.
Parameters
----------
scenario : :class:`stemp_abw.models.Scenario`
Scenario to read data from
Notes
-----
Data is taken from aggregated regional data of user scenario,
CONTROL_VALUES_MAP defines the mapping from controls' ids to the data
entry.
"""
reg_data = self.region_data_for_scenario(scenario)
# build value dict mapping between control id and data in scenario data dict
control_values = {}
for c_name, d_name in CONTROL_VALUES_MAP.items():
if isinstance(CONTROL_VALUES_MAP[c_name], str):
control_values[c_name] = reg_data[d_name]
elif isinstance(CONTROL_VALUES_MAP[c_name], list):
control_values[c_name] = sum([reg_data[d_name_2]
for d_name_2
in CONTROL_VALUES_MAP[c_name]])
return control_values
@staticmethod
def create_mun_data_ratio_for_aggregation():
"""Create table of technology shares for municipalities from status
quo scenario.
The scenario holds data for every municipality. In contrast, the UI
uses values for the entire region. Hence, the capacity ratio of a
certain parameter between municipality and entire region is needed for
aggregation (mun->region) or disaggragation (region->mun).
An instantaneous calculation is inappropriate as it leads to error
propagation.
"""
scn = Scenario.objects.get(name='Status quo')
scn_data = pd.DataFrame.from_dict(
json.loads(scn.data.data)['mun_data'],
orient='index')
return scn_data / scn_data.sum(axis=0)
def create_reg_tech_ratios(self):
"""Create table with share of specific technologies belonging to the
same category from status quo scenario.
The scenario holds data for specific sub-technonogies. In contrast,
the UI uses values for a superior technology (e.g. 'pv_roof' is split
into 'gen_capacity_pv_roof_large' and 'gen_capacity_pv_roof_small').
Hence, the capacity ratio of a certain sub-technology and its superior
technology is needed to determine when mapping between these two data
models.
An instantaneous calculation is inappropriate as it leads to error
propagation.
"""
reg_data = self.region_data_for_scenario(
Scenario.objects.get(name='Status quo'))
tech_ratios = {}
# find needed params for the mapping
for c_name, d_name in CONTROL_VALUES_MAP.items():
if isinstance(d_name, list):
for subtech in d_name:
tech_ratios[subtech] = reg_data[subtech] /\
sum([reg_data[_] for _ in d_name])
return tech_ratios
def update_scenario_data(self, ctrl_data=None):
"""Update municipal data of user scenario
Parameters
----------
ctrl_data : :obj:`dict`
Data to update in the scenario, e.g. {'sl_wind': 500}
Notes
-----
Keys of dictionary must be ids of UI controls (contained in mapping
dict CONTROL_VALUES_MAP). According to this mapping dict, some params
require changes of multiple entries in scenario data. This is done by
capacity-proportional change of those entries (see 2 below).
"""
if not isinstance(ctrl_data, dict) or len(ctrl_data) == 0:
raise ValueError('Data dict not specified or empty!')
reg_data_upd = {}
# calculate new regional params
for c_name, val in ctrl_data.items():
# 1) value to be set refers to a single entry (e.g. 'sl_wind')
if isinstance(CONTROL_VALUES_MAP[c_name], str):
reg_data_upd[CONTROL_VALUES_MAP[c_name]] = val
# 2) value to be set refers to a list of entries (e.g. a change of
# 'sl_pv_roof' needs changes of 'gen_capacity_pv_roof_large' and
# 'gen_capacity_pv_roof_small')
elif isinstance(CONTROL_VALUES_MAP[c_name], list):
for d_name in CONTROL_VALUES_MAP[c_name]:
reg_data_upd[d_name] = val * self.tech_ratios[d_name]
scn_data = json.loads(self.user_scenario.data.data)
# update regional data
for k, v in reg_data_upd.items():
if k in scn_data['reg_params']:
scn_data['reg_params'][k] = v
# update municipal data
for mun, v in self.__disaggregate_reg_to_mun_data(reg_data_upd).items():
scn_data['mun_data'][mun].update(v)
# updates at change of repowering scenario
if 'dd_repowering' in ctrl_data:
# 1) change repowering scn DB entry for scenario
self.user_scenario.repowering_scenario = \
RepoweringScenario.objects.get(
id=scn_data['reg_params']['repowering_scn'])
# 2) mun data update
repower_data = json.loads(
self.user_scenario.repowering_scenario.data)
# free sceario
if int(ctrl_data['dd_repowering']) == -1:
sl_wind_repower_pot = round(
sum([scn_data['mun_data'][mun]['gen_capacity_wind']
for mun in scn_data['mun_data'].keys()]
)
)
# other scenarios
else:
for mun in scn_data['mun_data']:
scn_data['mun_data'][mun]['gen_capacity_wind'] =\
repower_data[mun]['gen_capacity_wind']
# 3) calculate potential for wind slider update
sl_wind_repower_pot = \
round(sum([_['gen_capacity_wind']
for _ in repower_data.values()]))
else:
sl_wind_repower_pot = None
# update user scenario
self.user_scenario.data.data = json.dumps(scn_data,
sort_keys=True)
return sl_wind_repower_pot
def __disaggregate_reg_to_mun_data(self, reg_data):
"""Disaggregate and assign given regional data to given municipal data
# TODO: Insert notice that energy values are updated after sim + add reference
Parameters
----------
reg_data : :obj:`dict`
Regional data (updated)
"""
mun_data_upd = pd.DataFrame()
for param in reg_data:
if param in self.mun_to_reg_ratios.columns:
mun_data_upd[param] = (self.mun_to_reg_ratios[param] *
reg_data[param]).round(decimals=3)
return mun_data_upd.to_dict(orient='index')
# def __prepare_re_potential_
class Simulation(object):
"""Simulation data
TODO: Finish docstring
"""
def __init__(self, session):
self.esys = None
self.session = session
self.results = Results(simulation=self)
def create_esys(self):
"""Create energy system, parametrize and add nodes"""
# create esys
self.esys = solph.EnergySystem(
timeindex=pd.date_range(start=SIM_CFG['date_from'],
end=SIM_CFG['date_to'],
freq=SIM_CFG['freq']))
# create nodes from user scenario and add to energy system
self.esys.add(
*create_nodes(
**json.loads(
self.session.user_scenario.data.data
)
)
)
def load_or_simulate(self):
"""Load results from DB if existing, start simulation if not
Check if results are already in the DB using Scenario data's UUID
"""
user_scn_data_uuid = UUID(hashlib.md5(
self.session.user_scenario.data.data.encode('utf-8')).hexdigest())
# reverse lookup for scenario
if Scenario.objects.filter(data__data_uuid=user_scn_data_uuid).exists():
print('Scenario results found, load from DB...')
results_json = Scenario.objects.get(
data__data_uuid=user_scn_data_uuid).results.data
self.store_values(*oemof_json_to_results(results_json))
else:
print('Scenario results not found, start simulation...')
self.store_values(*simulate_energysystem(self.esys))
def store_values(self, results, param_results):
# update result raw data
self.results.set_result_raw_data(results_raw=results,
param_results_raw=param_results)
# update energy values of mun data
self.results.update_mun_energy_results_post_simulation()
# get layer results for user scn
self.results.layer_results =\
self.results.get_layer_results()
# TODO: save results to DB
class Tracker(object):
"""Tracker to store certain user activity
E.g. to show popups for features if the user has not visited a certain
part in the session.
"""
def __init__(self, session):
self.session = session
self.visited = self.__init_data()
def __init_data(self):
visited = {'esys': False,
'areas': False}
return visited