OpenDHW 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- OpenDHW/OpenDHW.py +1608 -0
- OpenDHW/__init__.py +9 -0
- OpenDHW/utils/OpenDHW_Utilities.py +255 -0
- OpenDHW/utils/__init__.py +4 -0
- OpenDHW-0.1.dist-info/LICENSE +21 -0
- OpenDHW-0.1.dist-info/METADATA +21 -0
- OpenDHW-0.1.dist-info/RECORD +9 -0
- OpenDHW-0.1.dist-info/WHEEL +5 -0
- OpenDHW-0.1.dist-info/top_level.txt +1 -0
OpenDHW/OpenDHW.py
ADDED
|
@@ -0,0 +1,1608 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import seaborn as sns
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import math
|
|
8
|
+
import statistics
|
|
9
|
+
import random
|
|
10
|
+
import scipy
|
|
11
|
+
from scipy.stats import beta
|
|
12
|
+
import matplotlib.dates as mdates
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
This is the script that stores all function of the DHWcalc package.
|
|
16
|
+
It is not meant to be executed on its own, but rather a toolbox for building
|
|
17
|
+
small scripts. Examples are given in OpenDHW/Examples.
|
|
18
|
+
|
|
19
|
+
OpenDHW is mostly built on Pandas, a good introduction is given here:
|
|
20
|
+
https://pandas.pydata.org/pandas-docs/stable/user_guide/10min.html
|
|
21
|
+
https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
|
|
22
|
+
|
|
23
|
+
OpenDHW_Utilities stores a few other functions that do not generate DHW
|
|
24
|
+
Timeseries directly, like the StorageLoad Function.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# RWTH colours
|
|
28
|
+
rwth_blue = "#00549F"
|
|
29
|
+
rwth_red = "#CC071E"
|
|
30
|
+
# sns.set_style("white")
|
|
31
|
+
sns.set_context("paper")
|
|
32
|
+
|
|
33
|
+
# --- Constants ---
|
|
34
|
+
rho = 980 / 1000 # kg/L for Water (at 60°C? at 10°C its = 1)
|
|
35
|
+
cp = 4180 # J/kgK
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def import_from_dhwcalc(s_step, daylight_saving, categories,
|
|
39
|
+
mean_drawoff_vol_per_day=200, max_flowrate=1200):
|
|
40
|
+
"""
|
|
41
|
+
DHWcalc yields Volume Flow TimeSeries (in Liters per hour).
|
|
42
|
+
|
|
43
|
+
:param s_step: int: resolution of file in seconds
|
|
44
|
+
:param categories: int: either '1' or '4'
|
|
45
|
+
:param mean_drawoff_vol_per_day: int: daily water demand in Liters
|
|
46
|
+
:param daylight_saving: Bool: apply daylight saving or not
|
|
47
|
+
:param max_flowrate: int: maximum water flowrate in L/h
|
|
48
|
+
|
|
49
|
+
:return timeseries_df: df: dataframe that holds the data
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if daylight_saving:
|
|
53
|
+
ds_string = 'ds'
|
|
54
|
+
else:
|
|
55
|
+
ds_string = 'nods'
|
|
56
|
+
|
|
57
|
+
# --- DHWcalc result files, saved in the OpenDHW Package
|
|
58
|
+
dhw_file = "{vol}L_{s_step}min_{cats}cat_sf_{ds}_max{max_flow}.txt".format(
|
|
59
|
+
vol=mean_drawoff_vol_per_day,
|
|
60
|
+
s_step=int(s_step / 60),
|
|
61
|
+
cats=categories,
|
|
62
|
+
ds=ds_string,
|
|
63
|
+
max_flow=max_flowrate,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
dhw_profile = Path.cwd().parent / "DHWcalc_Files" / dhw_file
|
|
67
|
+
|
|
68
|
+
assert dhw_profile.exists(), 'No DHWcalc File for the selected ' \
|
|
69
|
+
'parameters: {}'.format(dhw_file)
|
|
70
|
+
|
|
71
|
+
# Flowrate in Liter per Hour in each Step
|
|
72
|
+
water_LperH = [int(word.strip('\n')) for word in
|
|
73
|
+
open(dhw_profile).readlines()] # L/h each step
|
|
74
|
+
|
|
75
|
+
date_range = pd.date_range(start='2019-01-01', end='2020-01-01',
|
|
76
|
+
freq=str(s_step) + 'S')
|
|
77
|
+
date_range = date_range[:-1]
|
|
78
|
+
|
|
79
|
+
# make dataframe
|
|
80
|
+
timeseries_df = pd.DataFrame(water_LperH, index=date_range, columns=[
|
|
81
|
+
'Water_LperH'])
|
|
82
|
+
|
|
83
|
+
timeseries_df['Water_L'] = timeseries_df['Water_LperH'] / 3600 * s_step
|
|
84
|
+
timeseries_df['method'] = 'DHWcalc'
|
|
85
|
+
timeseries_df['mean_drawoff_vol_per_day'] = mean_drawoff_vol_per_day
|
|
86
|
+
timeseries_df['categories'] = categories
|
|
87
|
+
timeseries_df['initial_day'] = 0
|
|
88
|
+
timeseries_df['weekend_weekday_factor'] = 1.2
|
|
89
|
+
timeseries_df['sdtdev_drawoff_vol_per_day'] = mean_drawoff_vol_per_day / 4
|
|
90
|
+
|
|
91
|
+
if categories == 1:
|
|
92
|
+
mean_vol_per_drawoff = 8 # constant DHWcalc 1 category
|
|
93
|
+
timeseries_df['mean_vol_per_drawoff'] = 8
|
|
94
|
+
|
|
95
|
+
mean_drawoff_flow_rate_LperH = mean_vol_per_drawoff * 3600 / s_step
|
|
96
|
+
timeseries_df[
|
|
97
|
+
'mean_drawoff_flow_rate_LperH'] = mean_drawoff_flow_rate_LperH
|
|
98
|
+
|
|
99
|
+
sdt_dev_drawoff_flow_rate = mean_drawoff_flow_rate_LperH / 4 # in L/h
|
|
100
|
+
timeseries_df[
|
|
101
|
+
'sdtdev_drawoff_flow_rate_LperH'] = sdt_dev_drawoff_flow_rate
|
|
102
|
+
|
|
103
|
+
mean_no_drawoffs_per_day \
|
|
104
|
+
= mean_drawoff_vol_per_day / mean_vol_per_drawoff
|
|
105
|
+
timeseries_df['mean_no_drawoffs_per_day'] = mean_no_drawoffs_per_day
|
|
106
|
+
|
|
107
|
+
return timeseries_df
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def generate_dhw_profile(s_step, categories, holidays, mean_drawoff_vol_per_day=200, weekend_weekday_factor=1.2, initial_day=0):
|
|
111
|
+
"""
|
|
112
|
+
Generates a DHW profile. The generation is split up in different
|
|
113
|
+
functions and generally follows the methodology described in the DHWcalc
|
|
114
|
+
paper from Uni Kassel.
|
|
115
|
+
|
|
116
|
+
1) Load some data for the drawoff categories (cats_df).
|
|
117
|
+
2) Generate a yearly probability profile
|
|
118
|
+
3) Generate Drawoffs and distribute them randomly into the probability
|
|
119
|
+
profile p_norm_integral.
|
|
120
|
+
4) Add some additionally stats to the dataframe.
|
|
121
|
+
|
|
122
|
+
:param s_step: int: timestep width in seconds.
|
|
123
|
+
:param categories: int: 1 or 4 (see DHWcalc)
|
|
124
|
+
:param weekend_weekday_factor: int: taken from DHWcalc
|
|
125
|
+
:param mean_drawoff_vol_per_day: int: function of number of people in
|
|
126
|
+
the house of floor area.
|
|
127
|
+
:param initial_day: int: 0:Mon - 1:Tues ... 6:Sun
|
|
128
|
+
:return: timeseries_df df: dataframe with all timeseries
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
# --- holds statistic info about the drawoffs
|
|
132
|
+
cats_df = get_data_drawoff_categories(
|
|
133
|
+
s_step=s_step,
|
|
134
|
+
categories=categories,
|
|
135
|
+
mean_drawoff_vol_per_day=mean_drawoff_vol_per_day,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# --- deterministic function
|
|
139
|
+
timeseries_df = generate_yearly_probability_profile(
|
|
140
|
+
s_step=s_step,
|
|
141
|
+
weekend_weekday_factor=weekend_weekday_factor,
|
|
142
|
+
holidays = holidays,
|
|
143
|
+
initial_day=0,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# --- empty drawoffs list, will be filled afterwards
|
|
147
|
+
timeseries_df['Water_LperH'] = [0] * int(365 * 24 * 3600 / s_step)
|
|
148
|
+
|
|
149
|
+
# --- for each category, generate and distribute drawoffs.
|
|
150
|
+
for i in range(len(cats_df)):
|
|
151
|
+
timeseries_df = generate_and_distribute_drawoffs(
|
|
152
|
+
timeseries_df=timeseries_df,
|
|
153
|
+
cats_series=cats_df.iloc[i],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# --- add some additional stats
|
|
157
|
+
timeseries_df['Water_L'] = timeseries_df['Water_LperH'] / 3600 * s_step
|
|
158
|
+
timeseries_df['method'] = 'OpenDHW'
|
|
159
|
+
timeseries_df['categories'] = categories
|
|
160
|
+
timeseries_df['initial_day'] = initial_day
|
|
161
|
+
timeseries_df['weekend_weekday_factor'] = weekend_weekday_factor
|
|
162
|
+
timeseries_df['mean_drawoff_vol_per_day'] = mean_drawoff_vol_per_day
|
|
163
|
+
|
|
164
|
+
return timeseries_df
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_data_drawoff_categories(s_step, categories, mean_drawoff_vol_per_day):
|
|
168
|
+
"""
|
|
169
|
+
Get some data for each drawoff category. If only one category is chosen,
|
|
170
|
+
a simplified datafarme is returned.
|
|
171
|
+
|
|
172
|
+
:param s_step: int: seconds in a timestep. f.e 900
|
|
173
|
+
:param categories: int: 1 or 4, 1: short laod (washing hands, etc.), 2: medium load (dish-washer, etc.), 3: bath, 4:shower (see DHWcalc)
|
|
174
|
+
:param mean_drawoff_vol_per_day: int: volume per day used in house
|
|
175
|
+
:return: cats_df: df: Categores Data
|
|
176
|
+
"""
|
|
177
|
+
if categories == 4:
|
|
178
|
+
cats_data_60 = {'mean_flow_rate_per_drawoff_LperH': [60, 360, 840, 480],
|
|
179
|
+
'drawoff_duration_min': [1, 1, 10, 5],
|
|
180
|
+
'portion': [0.14, 0.36, 0.1, 0.4],
|
|
181
|
+
'stddev_flow_rate_per_drawoff_LperH': [120, 120, 12, 24],
|
|
182
|
+
'min_flow_rate_per_drawoff_LperH': [1, 1, 1, 1]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
cats_df = pd.DataFrame(data=cats_data_60)
|
|
186
|
+
# sort by duration distributes long drawoff types first.
|
|
187
|
+
# todo: second sort by portion, biggest portion distributed first
|
|
188
|
+
cats_df.sort_values(by=['drawoff_duration_min'], ascending=False,
|
|
189
|
+
inplace=True)
|
|
190
|
+
|
|
191
|
+
elif categories == 1:
|
|
192
|
+
cats_data_60 = {'mean_flow_rate_per_drawoff_LperH': [480],
|
|
193
|
+
'drawoff_duration_min': [1],
|
|
194
|
+
'portion': [1],
|
|
195
|
+
'stddev_flow_rate_per_drawoff_LperH': [120],
|
|
196
|
+
'min_flow_rate_per_drawoff_LperH': [6]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cats_df = pd.DataFrame(data=cats_data_60)
|
|
200
|
+
else:
|
|
201
|
+
raise Exception('unkown number of categories')
|
|
202
|
+
|
|
203
|
+
# if DHWcalc uses 4 categories with a timestep other than 60s,
|
|
204
|
+
# the drawoffs data has to be altered.
|
|
205
|
+
if s_step != 60:
|
|
206
|
+
cats_df['drawoff_duration_min_old'] = cats_df['drawoff_duration_min']
|
|
207
|
+
|
|
208
|
+
cats_df['drawoff_duration_min'] = int(s_step / 60)
|
|
209
|
+
|
|
210
|
+
cats_df['conversion_factor'] = cats_df['drawoff_duration_min'] / \
|
|
211
|
+
cats_df['drawoff_duration_min_old']
|
|
212
|
+
|
|
213
|
+
cats_df['mean_flow_rate_per_drawoff_LperH'] \
|
|
214
|
+
= cats_df['mean_flow_rate_per_drawoff_LperH'] / cats_df[
|
|
215
|
+
'conversion_factor']
|
|
216
|
+
cats_df['stddev_flow_rate_per_drawoff_LperH'] = \
|
|
217
|
+
cats_df['stddev_flow_rate_per_drawoff_LperH'] / cats_df[
|
|
218
|
+
'conversion_factor']
|
|
219
|
+
|
|
220
|
+
# add more data to the category dataframe.
|
|
221
|
+
cats_df['mean_vol_per_drawoff'] = \
|
|
222
|
+
cats_df['mean_flow_rate_per_drawoff_LperH'] \
|
|
223
|
+
/ 60 * cats_df['drawoff_duration_min']
|
|
224
|
+
|
|
225
|
+
cats_df['mean_vol_per_day'] = mean_drawoff_vol_per_day * cats_df['portion']
|
|
226
|
+
|
|
227
|
+
cats_df['mean_vol_per_year'] = cats_df['mean_vol_per_day'] * 365
|
|
228
|
+
|
|
229
|
+
cats_df['mean_no_drawoffs_per_day'] = \
|
|
230
|
+
cats_df['mean_vol_per_day'] / cats_df['mean_vol_per_drawoff']
|
|
231
|
+
|
|
232
|
+
cats_df['mean_no_drawoffs_per_year'] = \
|
|
233
|
+
cats_df['mean_no_drawoffs_per_day'] * 365
|
|
234
|
+
|
|
235
|
+
# add max flow rate: Max(1200, highest category mean flow rate)
|
|
236
|
+
cats_df['max_flow_rate_per_drawoff_LperH'] \
|
|
237
|
+
= max(cats_df['mean_flow_rate_per_drawoff_LperH'].max(), 1200)
|
|
238
|
+
|
|
239
|
+
return cats_df
|
|
240
|
+
|
|
241
|
+
def generate_daily_probability_step_function(mode, s_step, save_fig=False,
|
|
242
|
+
test_concentrated_ps=False):
|
|
243
|
+
"""
|
|
244
|
+
Generates probabilities for a day with 6 periods. Corresponds to the mode
|
|
245
|
+
"step function for weekdays and weekends" in DHWcalc and uses the same
|
|
246
|
+
standard values. Each Day starts at 0:00. Steps in hours. Sum of steps
|
|
247
|
+
has to be 24. Sum of probabilities has to be 1.
|
|
248
|
+
|
|
249
|
+
:param test_concentrated_ps: bool: different probabilities,
|
|
250
|
+
very concentrated in the morning
|
|
251
|
+
:param mode: string: weekday or weekend day
|
|
252
|
+
:param s_step: int: seconds within a timestep
|
|
253
|
+
:param save_fig: Bool: plot the probability distribution
|
|
254
|
+
:return: p_day list: distribution for one day.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
# todo: add profiles for non-residential buildings, no more heavy periods
|
|
258
|
+
# in the morning and evening? different for every industry type? more
|
|
259
|
+
# during the night?
|
|
260
|
+
|
|
261
|
+
if s_step <= 1800:
|
|
262
|
+
if mode == 'work-day':
|
|
263
|
+
steps_and_ps = [(6.5, 0.01), (1, 0.5), (4.5, 0.06), (1, 0.16),
|
|
264
|
+
(5, 0.06), (4, 0.2), (2, 0.01)]
|
|
265
|
+
|
|
266
|
+
elif mode == 'off-day':
|
|
267
|
+
steps_and_ps = [(7, 0.02), (2, 0.475), (6, 0.071), (2, 0.237),
|
|
268
|
+
(3, 0.036), (3, 0.143), (1, 0.018)]
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
raise Exception('Unknown Mode. Please Choose "work-day" or '
|
|
272
|
+
'"off-day".')
|
|
273
|
+
else:
|
|
274
|
+
# no more half-hourly steps
|
|
275
|
+
if mode == 'work-day':
|
|
276
|
+
steps_and_ps = [(7, 0.01), (1, 0.5), (4, 0.06), (1, 0.16),
|
|
277
|
+
(5, 0.06), (4, 0.2), (2, 0.01)]
|
|
278
|
+
|
|
279
|
+
elif mode == 'off-day':
|
|
280
|
+
steps_and_ps = [(7, 0.02), (2, 0.475), (6, 0.071), (2, 0.237),
|
|
281
|
+
(3, 0.036), (3, 0.143), (1, 0.018)]
|
|
282
|
+
|
|
283
|
+
else:
|
|
284
|
+
raise Exception('Unknown Mode. Please Choose "work-day" or '
|
|
285
|
+
'"off-day".')
|
|
286
|
+
|
|
287
|
+
if test_concentrated_ps:
|
|
288
|
+
# just as a test, if p is very concentrated, only 2 hours in the morning
|
|
289
|
+
steps_and_ps = [(7, 0), (2, 1), (15, 0)]
|
|
290
|
+
|
|
291
|
+
steps = [tup[0] for tup in steps_and_ps]
|
|
292
|
+
ps = [tup[1] for tup in steps_and_ps]
|
|
293
|
+
|
|
294
|
+
assert sum(steps) == 24
|
|
295
|
+
assert sum(ps) == 1
|
|
296
|
+
|
|
297
|
+
p_day = []
|
|
298
|
+
|
|
299
|
+
for tup in steps_and_ps:
|
|
300
|
+
p_lst = [tup[1] for _ in range(int(tup[0] * 3600 / s_step))]
|
|
301
|
+
p_day.extend(p_lst)
|
|
302
|
+
|
|
303
|
+
# check if length of daily intervals fits into the stepwidth.
|
|
304
|
+
assert len(p_day) == 24 * 3600 / s_step
|
|
305
|
+
|
|
306
|
+
if save_fig:
|
|
307
|
+
fig, ax = plt.subplots()
|
|
308
|
+
plt.plot(p_day)
|
|
309
|
+
plt.show()
|
|
310
|
+
dir_output = Path.cwd() / "plots"
|
|
311
|
+
dir_output.mkdir(exist_ok=True)
|
|
312
|
+
fname = "Daily_Probability_Profile_{}S_{}".format(s_step, mode)
|
|
313
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
314
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
315
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
316
|
+
|
|
317
|
+
return p_day
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def generate_yearly_probability_profile(s_step, holidays, weekend_weekday_factor=1.2,
|
|
321
|
+
initial_day=0):
|
|
322
|
+
"""
|
|
323
|
+
generate a summed yearly probability profile. The whole function is
|
|
324
|
+
deterministic. The same inputs always produce the same outputs.
|
|
325
|
+
|
|
326
|
+
1) Probabilities for weekdays and weekend-days are loaded (p_we, p_wd).
|
|
327
|
+
2) Probability of weekend-days is increased relative to weekdays (shift).
|
|
328
|
+
3) Based on an initial day, the yearly probability distribution (p_final)
|
|
329
|
+
is generated. The seasonal influence is modelled by a sine-function.
|
|
330
|
+
4) p_final is normalized and integrated. The sum over the year is thus
|
|
331
|
+
equal to 1 (p_norm_integral).
|
|
332
|
+
|
|
333
|
+
:param s_step: int: seconds in a timestep
|
|
334
|
+
:param weekend_weekday_factor: float: shift probabilities towards weekend
|
|
335
|
+
:param initial_day: int: Mon: 0 ... Sun: 6
|
|
336
|
+
:return: timeseries_df: df: df that holds the yearly profile
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
# load daily probabilities (deterministic)
|
|
340
|
+
p_we = generate_daily_probability_step_function(
|
|
341
|
+
mode='off-day',
|
|
342
|
+
s_step=s_step,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
p_wd = generate_daily_probability_step_function(
|
|
346
|
+
mode='work-day',
|
|
347
|
+
s_step=s_step,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# shift towards weekend (deterministic)
|
|
351
|
+
p_wd_weighted, p_we_weighted, av_p_week_weighted = shift_weekend_weekday(
|
|
352
|
+
p_work_day=p_wd,
|
|
353
|
+
p_off_day=p_we,
|
|
354
|
+
factor=weekend_weekday_factor
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# yearly curve (deterministic)
|
|
358
|
+
p_final = generate_yearly_probabilities(
|
|
359
|
+
initial_day=initial_day,
|
|
360
|
+
p_off_day=p_we_weighted,
|
|
361
|
+
p_work_day=p_wd_weighted,
|
|
362
|
+
s_step=s_step,
|
|
363
|
+
holidays=holidays,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# sum and normalize to range between 0 and 1.
|
|
367
|
+
p_norm_integral = normalize_and_sum_list(lst=p_final)
|
|
368
|
+
|
|
369
|
+
# make timeseries dataframe and append the final list
|
|
370
|
+
date_range = pd.date_range(start='2019-01-01', end='2020-01-01',
|
|
371
|
+
freq=str(s_step) + 'S')
|
|
372
|
+
date_range = date_range[:-1]
|
|
373
|
+
|
|
374
|
+
timeseries_df = pd.DataFrame(index=date_range,
|
|
375
|
+
data={'p_norm_integral': p_norm_integral})
|
|
376
|
+
|
|
377
|
+
return timeseries_df
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def shift_weekend_weekday(p_work_day, p_off_day, factor):
|
|
381
|
+
"""
|
|
382
|
+
Shifts the probabilities between the weekday list and the weekend list by a
|
|
383
|
+
defined factor. If the factor is bigger than 1, the probability on the
|
|
384
|
+
weekend is increased. If its smaller than 1, the probability on the
|
|
385
|
+
weekend is decreased.
|
|
386
|
+
|
|
387
|
+
:param p_work_day: list: probabilities for 1 work day of the week [0...1]
|
|
388
|
+
:param p_off_day: list: probabilities for 1 off day of the week [0...1]
|
|
389
|
+
:param factor: float: factor to shift the probabilities between
|
|
390
|
+
weekdays and weekend-days
|
|
391
|
+
:return:
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
p_wd_factor = 1 / (5 / 7 + factor * 2 / 7)
|
|
395
|
+
p_we_factor = 1 / (1 / factor * 5 / 7 + 2 / 7)
|
|
396
|
+
|
|
397
|
+
assert p_wd_factor * 5 / 7 + p_we_factor * 2 / 7 == 1
|
|
398
|
+
|
|
399
|
+
p_wd_weighted = [p * p_we_factor for p in p_work_day]
|
|
400
|
+
p_we_weighted = [p * p_we_factor for p in p_off_day]
|
|
401
|
+
|
|
402
|
+
av_p_wd_weighted = statistics.mean(p_wd_weighted)
|
|
403
|
+
av_p_we_weighted = statistics.mean(p_we_weighted)
|
|
404
|
+
|
|
405
|
+
av_p_week_weighted = av_p_wd_weighted * 5 / 7 + av_p_we_weighted * 2 / 7
|
|
406
|
+
|
|
407
|
+
return p_wd_weighted, p_we_weighted, av_p_week_weighted
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def generate_yearly_probabilities(initial_day, p_off_day, p_work_day,
|
|
411
|
+
s_step, holidays, plot_p_yearly=False):
|
|
412
|
+
"""
|
|
413
|
+
Takes the probabilities of a working days and a off days and generates a
|
|
414
|
+
list of yearly probabilities by adding a seasonal probability factor.
|
|
415
|
+
The seasonal factor is a sine-function, like in DHWcalc.
|
|
416
|
+
|
|
417
|
+
:param initial_day: int: 0: Mon, 1: Tue, 2: Wed, 3: Thur, 4: Fri,
|
|
418
|
+
5 : Sat, 6 : Sun
|
|
419
|
+
:param p_off_day: list: probabilities of an off day
|
|
420
|
+
:param p_work_day: list: probabilities of a working day
|
|
421
|
+
:param s_step: int: seconds within a timestep
|
|
422
|
+
:param plot_p_yearly: bool: plot the yearly probabilities
|
|
423
|
+
|
|
424
|
+
:return: p_final: list: probabilities of a full year
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
p_final = []
|
|
428
|
+
timesteps_day = int(24 * 3600 / s_step)
|
|
429
|
+
|
|
430
|
+
for day in range(365):
|
|
431
|
+
|
|
432
|
+
# Define if the day is a working day or not
|
|
433
|
+
if (day + initial_day) % 7 in (0, 6) or (day + initial_day) in holidays:
|
|
434
|
+
p_day = p_off_day
|
|
435
|
+
else:
|
|
436
|
+
p_day = p_work_day
|
|
437
|
+
|
|
438
|
+
# Compute seasonal factor
|
|
439
|
+
arg = math.pi * (2 / 365 * day - 1 / 4)
|
|
440
|
+
probability_season = 1 + 0.1 * np.cos(arg)
|
|
441
|
+
|
|
442
|
+
for step in range(timesteps_day):
|
|
443
|
+
probability = p_day[step] * probability_season
|
|
444
|
+
p_final.append(probability)
|
|
445
|
+
|
|
446
|
+
if plot_p_yearly:
|
|
447
|
+
fig, ax = plt.subplots()
|
|
448
|
+
plt.plot(p_final)
|
|
449
|
+
plt.show()
|
|
450
|
+
dir_output = Path.cwd() / "plots"
|
|
451
|
+
dir_output.mkdir(exist_ok=True)
|
|
452
|
+
fname = "Yearly_Probability_Profile_{}initalday_{}S".format(
|
|
453
|
+
initial_day, s_step)
|
|
454
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
455
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
456
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
457
|
+
|
|
458
|
+
return p_final
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def normalize_and_sum_list(lst, save_fig=False):
|
|
462
|
+
"""
|
|
463
|
+
takes a list and normalizes it based on the sum of all list elements.
|
|
464
|
+
then generates a new list based on the current sum of each list entry.
|
|
465
|
+
|
|
466
|
+
:param lst: list: input list
|
|
467
|
+
:param save_fig: bool: plot the output list
|
|
468
|
+
:return: lst_norm_integral: list output list
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
sum_lst = sum(lst)
|
|
472
|
+
lst_norm = [float(i) / sum_lst for i in lst]
|
|
473
|
+
|
|
474
|
+
current_sum = 0
|
|
475
|
+
lst_norm_integral = []
|
|
476
|
+
|
|
477
|
+
for entry in lst_norm:
|
|
478
|
+
current_sum += entry
|
|
479
|
+
lst_norm_integral.append(current_sum)
|
|
480
|
+
|
|
481
|
+
if save_fig:
|
|
482
|
+
fig, ax = plt.subplots()
|
|
483
|
+
plt.plot(lst_norm_integral)
|
|
484
|
+
plt.show()
|
|
485
|
+
dir_output = Path.cwd() / "plots"
|
|
486
|
+
dir_output.mkdir(exist_ok=True)
|
|
487
|
+
fname = "Normed_and_summed_probability_profile"
|
|
488
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
489
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
490
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
491
|
+
|
|
492
|
+
return lst_norm_integral
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def generate_and_distribute_drawoffs(timeseries_df, cats_series):
|
|
496
|
+
"""
|
|
497
|
+
generate and distribute drawoffs
|
|
498
|
+
|
|
499
|
+
:param timeseries_df: df: holds the timeseries
|
|
500
|
+
:param cats_series: series: constants for a category
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
# --- compute how many timesteps the drawoff occupies. some take more than 1
|
|
504
|
+
s_step = int(timeseries_df.index.freqstr[:-1])
|
|
505
|
+
drawoff_duration = cats_series['drawoff_duration_min'] * 60
|
|
506
|
+
drawoff_steps = int(drawoff_duration / s_step)
|
|
507
|
+
|
|
508
|
+
# --- generate drawoffs until V_max is reached ---
|
|
509
|
+
V_curr = 0
|
|
510
|
+
V_max = cats_series['mean_vol_per_year']
|
|
511
|
+
drawoffs = [] # L/h
|
|
512
|
+
|
|
513
|
+
while V_curr <= V_max:
|
|
514
|
+
drawoff = generate_single_drawoff_inside_boundaries(cats_series, s_step)
|
|
515
|
+
drawoffs.append(drawoff)
|
|
516
|
+
|
|
517
|
+
drawoff_L = drawoff / 3600 * s_step * drawoff_steps # L
|
|
518
|
+
V_curr += drawoff_L
|
|
519
|
+
|
|
520
|
+
# --- generate a probability for each drawoff ---
|
|
521
|
+
p_norm_integral = list(timeseries_df['p_norm_integral'])
|
|
522
|
+
min_rand = min(p_norm_integral)
|
|
523
|
+
max_rand = max(p_norm_integral)
|
|
524
|
+
p_drawoffs = [random.uniform(min_rand, max_rand) for _ in range(
|
|
525
|
+
len(drawoffs))]
|
|
526
|
+
|
|
527
|
+
# --- sort both lists for the distribution algorithm ---
|
|
528
|
+
p_drawoffs.sort()
|
|
529
|
+
p_norm_integral.sort()
|
|
530
|
+
|
|
531
|
+
# --- distribute drawoffs ---
|
|
532
|
+
water_LperH_cat = [0] * int(365 * 24 * 3600 / s_step)
|
|
533
|
+
water_LperH = list(timeseries_df['Water_LperH'])
|
|
534
|
+
max_flow_rate = cats_series['max_flow_rate_per_drawoff_LperH']
|
|
535
|
+
|
|
536
|
+
# counter for the drawoffs
|
|
537
|
+
drawoff_count = 0
|
|
538
|
+
|
|
539
|
+
# loop p_norm_integral and place drawoffs:
|
|
540
|
+
for time_step, p_current_sum in enumerate(p_norm_integral):
|
|
541
|
+
|
|
542
|
+
# dont place drawoff if timestep has already reached the max flowrate
|
|
543
|
+
if water_LperH[time_step] >= max_flow_rate:
|
|
544
|
+
continue
|
|
545
|
+
|
|
546
|
+
# if all drawoffs are palced, break the loop.
|
|
547
|
+
if drawoff_count >= len(drawoffs):
|
|
548
|
+
break
|
|
549
|
+
|
|
550
|
+
# if the looping of p_norm_integral results in surpassing the
|
|
551
|
+
# probability of the chosen drawoff, that drawoff might be placed at
|
|
552
|
+
# that timestep!
|
|
553
|
+
# This while loop allows for the possibility that two draw-offs from the same
|
|
554
|
+
# category can be added together at the same timestep if two consecutive elements
|
|
555
|
+
# (or even more) of p_drawoffs are lower than p_current_sum at this timestep.
|
|
556
|
+
while p_drawoffs[drawoff_count] < p_current_sum:
|
|
557
|
+
|
|
558
|
+
# if the drawoff event occupies more than one timestep,
|
|
559
|
+
# a list (drawoffs_time_step_delta) is placed, rather than a
|
|
560
|
+
# single number.
|
|
561
|
+
drawoff = drawoffs[drawoff_count]
|
|
562
|
+
drawoffs_time_step_delta = [drawoff] * drawoff_steps
|
|
563
|
+
|
|
564
|
+
# boolean, to count the drawoff events, but not the timesteps
|
|
565
|
+
# occupied by all drawoffs. this is needed when the drawoff
|
|
566
|
+
# event occupies more than one timestep.
|
|
567
|
+
drawoff_occured = True
|
|
568
|
+
|
|
569
|
+
for i in range(drawoff_steps):
|
|
570
|
+
|
|
571
|
+
# if the added drawoff surpasses the max flowrate in any of
|
|
572
|
+
# the possible timesteps it would occupy, it should not occur!
|
|
573
|
+
if water_LperH[time_step + i] + \
|
|
574
|
+
drawoffs_time_step_delta[i] > max_flow_rate:
|
|
575
|
+
drawoff_occured = False
|
|
576
|
+
break
|
|
577
|
+
|
|
578
|
+
if drawoff_occured:
|
|
579
|
+
for i in range(drawoff_steps):
|
|
580
|
+
# if the added drawoff would not surpass the max
|
|
581
|
+
# flowrate, add it to both return lists! for the
|
|
582
|
+
# category, and for the whole list.
|
|
583
|
+
water_LperH[time_step + i] \
|
|
584
|
+
+= drawoffs_time_step_delta[i]
|
|
585
|
+
water_LperH_cat[time_step + i] \
|
|
586
|
+
+= drawoffs_time_step_delta[i]
|
|
587
|
+
|
|
588
|
+
drawoff_count += 1
|
|
589
|
+
|
|
590
|
+
else:
|
|
591
|
+
break # break the while loop.
|
|
592
|
+
|
|
593
|
+
if drawoff_count >= len(drawoffs):
|
|
594
|
+
break
|
|
595
|
+
|
|
596
|
+
# update the sum of all categories
|
|
597
|
+
timeseries_df['Water_LperH'] = water_LperH
|
|
598
|
+
|
|
599
|
+
# write the drawoff list for the current category to the df
|
|
600
|
+
cat_id = int(cats_series['mean_flow_rate_per_drawoff_LperH'])
|
|
601
|
+
timeseries_df['Water_LperH_cat{}'.format(cat_id)] = water_LperH_cat
|
|
602
|
+
|
|
603
|
+
# compute the amount of water for the category
|
|
604
|
+
timeseries_df['Water_L_cat{}'.format(cat_id)] = \
|
|
605
|
+
timeseries_df['Water_LperH_cat{}'.format(cat_id)] * s_step / 3600
|
|
606
|
+
|
|
607
|
+
return timeseries_df
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def generate_single_drawoff_inside_boundaries(cats_series, s_step):
|
|
611
|
+
"""
|
|
612
|
+
From the data of one category, generate a drawoff inside the defined
|
|
613
|
+
boundaries, similar to DHWcalc.
|
|
614
|
+
|
|
615
|
+
:param cats_series: df: pandas series that holds the drawoff data
|
|
616
|
+
:param s_step: int: seconds in a timestep
|
|
617
|
+
:return: drawoff: int: drawoff eevnt in L/h
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
# --- get mean and stddev from series ---
|
|
621
|
+
mu = cats_series['mean_flow_rate_per_drawoff_LperH'] # in L/h
|
|
622
|
+
sig = cats_series['stddev_flow_rate_per_drawoff_LperH'] # in L/h
|
|
623
|
+
|
|
624
|
+
# --- generate drawoff
|
|
625
|
+
drawoff = random.gauss(mu, sig)
|
|
626
|
+
|
|
627
|
+
# --- get min and max allowed flowrate
|
|
628
|
+
max_drawoff_flow_rate = cats_series['max_flow_rate_per_drawoff_LperH']
|
|
629
|
+
min_drawoff_flow_rate = cats_series['min_flow_rate_per_drawoff_LperH']
|
|
630
|
+
|
|
631
|
+
# --- set boundaries for drawoff
|
|
632
|
+
low_lim = max(float(mu - 2 * sig), min_drawoff_flow_rate)
|
|
633
|
+
up_lim = min(float(mu + 2 * sig), max_drawoff_flow_rate)
|
|
634
|
+
|
|
635
|
+
# --- if drawoff is outside boundaries, generate it again until its inside.
|
|
636
|
+
while drawoff < low_lim or drawoff > up_lim:
|
|
637
|
+
drawoff = random.gauss(mu, sig)
|
|
638
|
+
|
|
639
|
+
# --- DHWcalc uses a fixed flow rate step width rather than floats.
|
|
640
|
+
if s_step == 60:
|
|
641
|
+
flow_rate_step = 6
|
|
642
|
+
else:
|
|
643
|
+
flow_rate_step = 1
|
|
644
|
+
drawoff = flow_rate_step * round(drawoff / flow_rate_step)
|
|
645
|
+
|
|
646
|
+
return drawoff # in L/h
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def compute_heat(timeseries_df, temp_dT):
|
|
650
|
+
"""
|
|
651
|
+
Add heat columns to the timeseries
|
|
652
|
+
|
|
653
|
+
:param timeseries_df: df: Pandas Dataframe with all the timeseries
|
|
654
|
+
:param temp_dT: int: temperature difference between freshwater
|
|
655
|
+
and average DHW outlet temperature.
|
|
656
|
+
|
|
657
|
+
:return: timeseries_df: df: Dataframe with added 'Heat' Column
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
timeseries_df['Heat_W'] = \
|
|
661
|
+
timeseries_df['Water_LperH'] / 3600 * rho * cp * temp_dT
|
|
662
|
+
timeseries_df['Heat_kW'] = timeseries_df['Heat_W'] / 1000
|
|
663
|
+
|
|
664
|
+
s_step = int(timeseries_df.index.freqstr[:-1])
|
|
665
|
+
timeseries_df['Heat_J'] = timeseries_df['Heat_W'] * s_step
|
|
666
|
+
timeseries_df['Heat_kWh'] = timeseries_df['Heat_J'] / (3600 * 1000)
|
|
667
|
+
|
|
668
|
+
return timeseries_df
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def draw_lineplot(timeseries_df, plot_var='water', start_plot='2019-02-01',
|
|
672
|
+
end_plot='2019-02-05', save_fig=False):
|
|
673
|
+
"""
|
|
674
|
+
Plots the timeseries for a given timedelta in a year.
|
|
675
|
+
|
|
676
|
+
:param timeseries_df: df: Dataframe that holds the timeseries.
|
|
677
|
+
:param plot_var: str: choose to plot Water or Heat series.
|
|
678
|
+
:param start_plot: str: start date of the plot. F.e. 2019-01-01
|
|
679
|
+
:param end_plot: str: end date of the plot. F.e. 2019-01-07
|
|
680
|
+
:param save_fig: bool: decide to save plots as pdf
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
fig, ax1 = plt.subplots()
|
|
684
|
+
fig.tight_layout()
|
|
685
|
+
|
|
686
|
+
if plot_var == 'water':
|
|
687
|
+
# make subset of dataframe for plotting
|
|
688
|
+
plot_df = timeseries_df[['Water_LperH', 'mean_drawoff_vol_per_day']]
|
|
689
|
+
|
|
690
|
+
ax1 = sns.lineplot(ax=ax1, data=plot_df[start_plot:end_plot],
|
|
691
|
+
linewidth=1.0, palette=[rwth_blue, rwth_red])
|
|
692
|
+
|
|
693
|
+
ax1.legend(loc="upper left")
|
|
694
|
+
|
|
695
|
+
title_str = make_title_str(timeseries_df=timeseries_df)
|
|
696
|
+
ax1.set_title(title_str)
|
|
697
|
+
|
|
698
|
+
if plot_var == 'heat':
|
|
699
|
+
# make subset of dataframe for plotting
|
|
700
|
+
plot_df = timeseries_df[['Heat_W']]
|
|
701
|
+
|
|
702
|
+
ax1 = sns.lineplot(ax=ax1, data=plot_df[start_plot:end_plot],
|
|
703
|
+
linewidth=1.0, palette=[rwth_red])
|
|
704
|
+
|
|
705
|
+
ax1.legend(loc="upper left")
|
|
706
|
+
|
|
707
|
+
# compute some stats for figure title.
|
|
708
|
+
# todo: add to make_title_str function for Heat plots.
|
|
709
|
+
max_water_flow = timeseries_df['Water_LperH'].max() # in L/h
|
|
710
|
+
s_step = timeseries_df.index.freqstr
|
|
711
|
+
method = timeseries_df['method'][0]
|
|
712
|
+
|
|
713
|
+
plt.title('Heat Time-series from {}, timestep = {}\n'
|
|
714
|
+
'with a Peak of {:.1f} L/h'.format(method, s_step,
|
|
715
|
+
max_water_flow))
|
|
716
|
+
|
|
717
|
+
# set the x axis ticks
|
|
718
|
+
# https://matplotlib.org/3.1.1/gallery/ticks_and_spines/date_concise_formatter.html
|
|
719
|
+
locator = mdates.AutoDateLocator()
|
|
720
|
+
formatter = mdates.ConciseDateFormatter(locator)
|
|
721
|
+
ax1.xaxis.set_major_locator(locator)
|
|
722
|
+
ax1.xaxis.set_major_formatter(formatter)
|
|
723
|
+
|
|
724
|
+
plt.show()
|
|
725
|
+
|
|
726
|
+
if save_fig:
|
|
727
|
+
method = timeseries_df['method'][0]
|
|
728
|
+
s_step = get_s_step(timeseries_df)
|
|
729
|
+
vol_per_day = timeseries_df['mean_drawoff_vol_per_day'][0]
|
|
730
|
+
cats = timeseries_df['categories'][0]
|
|
731
|
+
|
|
732
|
+
dir_output = Path.cwd() / "plots"
|
|
733
|
+
dir_output.mkdir(exist_ok=True)
|
|
734
|
+
|
|
735
|
+
fname = "Lineplot_{}_{}S_{}LperDay_{}cats".format(
|
|
736
|
+
method, s_step, vol_per_day, cats)
|
|
737
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
738
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
739
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def draw_histplot(timeseries_df, extra_kde=False, save_fig=False):
|
|
743
|
+
"""
|
|
744
|
+
Takes a DHW profile and plots a histogram with some stats in the title
|
|
745
|
+
|
|
746
|
+
:param save_fig: bool: save the figure
|
|
747
|
+
:param timeseries_df: df: Dataframe that holds the water timeseries
|
|
748
|
+
:param extra_kde: bool: plot a detailed kde plot behind the main
|
|
749
|
+
histogram.
|
|
750
|
+
"""
|
|
751
|
+
|
|
752
|
+
# get non-zero values of the profile
|
|
753
|
+
drawoffs_df = get_drawoffs(timeseries_df=timeseries_df, remove_cats=False)
|
|
754
|
+
|
|
755
|
+
cats = timeseries_df['categories'][0]
|
|
756
|
+
if cats == 1:
|
|
757
|
+
drawoffs_df = drawoffs_df['Water_LperH']
|
|
758
|
+
|
|
759
|
+
fig, ax1 = plt.subplots()
|
|
760
|
+
ax2 = ax1.twinx()
|
|
761
|
+
|
|
762
|
+
# https://seaborn.pydata.org/generated/seaborn.histplot.html
|
|
763
|
+
sns.histplot(data=drawoffs_df, ax=ax2, stat='count', kde=True,
|
|
764
|
+
kde_kws={'bw_adjust': 1})
|
|
765
|
+
|
|
766
|
+
if extra_kde:
|
|
767
|
+
# https://seaborn.pydata.org/generated/seaborn.kdeplot.html
|
|
768
|
+
sns.kdeplot(data=drawoffs_df, ax=ax1, alpha=.05, bw_adjust=0.05,
|
|
769
|
+
legend=False, color='r')
|
|
770
|
+
|
|
771
|
+
# title
|
|
772
|
+
title_str = make_title_str(timeseries_df=timeseries_df)
|
|
773
|
+
ax1.set_title(title_str)
|
|
774
|
+
|
|
775
|
+
plt.show()
|
|
776
|
+
|
|
777
|
+
if save_fig:
|
|
778
|
+
method = timeseries_df['method'][0]
|
|
779
|
+
s_step = get_s_step(timeseries_df)
|
|
780
|
+
vol_per_day = timeseries_df['mean_drawoff_vol_per_day'][0]
|
|
781
|
+
cats = timeseries_df['categories'][0]
|
|
782
|
+
|
|
783
|
+
dir_output = Path.cwd() / "plots"
|
|
784
|
+
dir_output.mkdir(exist_ok=True)
|
|
785
|
+
|
|
786
|
+
fname = "Histplot_{}_{}S_{}LperDay_{}cats".format(
|
|
787
|
+
method, s_step, vol_per_day, cats)
|
|
788
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
789
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
790
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def draw_detailed_histplot(timeseries_df):
|
|
794
|
+
"""
|
|
795
|
+
https://towardsdatascience.com/advanced-histogram-using-python-bceae288e715
|
|
796
|
+
plot to further analyse timeseries with 1 drawoff category.
|
|
797
|
+
"""
|
|
798
|
+
cats = timeseries_df['categories'][0]
|
|
799
|
+
method = timeseries_df['method'][0]
|
|
800
|
+
|
|
801
|
+
if cats == 1 and method == 'DHWcalc':
|
|
802
|
+
|
|
803
|
+
# create bin values
|
|
804
|
+
mean = timeseries_df['mean_drawoff_flow_rate_LperH'][0]
|
|
805
|
+
sdtdev = timeseries_df['sdtdev_drawoff_flow_rate_LperH'][0]
|
|
806
|
+
non_zero_min = timeseries_df[timeseries_df['Water_LperH'] > 0][
|
|
807
|
+
'Water_LperH'].min() # smallest entry that is not 0.
|
|
808
|
+
|
|
809
|
+
bin_values = [non_zero_min,
|
|
810
|
+
mean - 2 * sdtdev,
|
|
811
|
+
mean - sdtdev,
|
|
812
|
+
mean,
|
|
813
|
+
mean + sdtdev,
|
|
814
|
+
mean + 2 * sdtdev,
|
|
815
|
+
timeseries_df['Water_LperH'].max()]
|
|
816
|
+
bin_values = list(set(bin_values)) # remove double entries
|
|
817
|
+
bin_values.sort() # bins have to be sorted
|
|
818
|
+
|
|
819
|
+
# get non-zero values of the profile
|
|
820
|
+
drawoffs = timeseries_df[timeseries_df['Water_LperH'] != 0][
|
|
821
|
+
'Water_LperH']
|
|
822
|
+
|
|
823
|
+
# Plot the Histogram from the random data
|
|
824
|
+
fig, (ax) = plt.subplots()
|
|
825
|
+
|
|
826
|
+
# counts: count of data ponts for each bin/column in the histogram
|
|
827
|
+
# bins: bin edge/range values
|
|
828
|
+
# patches: list of Patch objects. each Patch object contains a
|
|
829
|
+
# Rectangle object. e.g. Rectangle(xy=(-2.51953, 0), width=0.501013,
|
|
830
|
+
# height=3, angle=0)
|
|
831
|
+
counts, bins, patches = ax.hist(drawoffs, bins=bin_values,
|
|
832
|
+
edgecolor='black')
|
|
833
|
+
|
|
834
|
+
# Set the ticks to be at the edges of the bins.
|
|
835
|
+
ax.set_xticks(bins.round(2))
|
|
836
|
+
|
|
837
|
+
# Set the graph title and axes titles
|
|
838
|
+
plt.ylabel('Count')
|
|
839
|
+
plt.xlabel('Flowrate L/h')
|
|
840
|
+
|
|
841
|
+
# Calculate bar centre to display the count of data points and %
|
|
842
|
+
bin_x_centers = 0.5 * np.diff(bins) + bins[:-1]
|
|
843
|
+
bin_y_centers = ax.get_yticks()[1] * 0.25
|
|
844
|
+
|
|
845
|
+
# Display the the count of data points and % for each bar in histogram
|
|
846
|
+
for i in range(len(bins) - 1):
|
|
847
|
+
bin_label = "{0:,}".format(counts[i]) + " ({0:.2f}%)".format(
|
|
848
|
+
(counts[i] / counts.sum()) * 100)
|
|
849
|
+
plt.text(bin_x_centers[i], bin_y_centers, bin_label, rotation=90,
|
|
850
|
+
rotation_mode='anchor')
|
|
851
|
+
|
|
852
|
+
# Display the graph
|
|
853
|
+
plt.show()
|
|
854
|
+
|
|
855
|
+
else:
|
|
856
|
+
print('detailed histplot is only meant to analyse DHWcalc timeseries '
|
|
857
|
+
'with one drawoff category.')
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def add_additional_runs(timeseries_df, holidays, total_runs=5, dir_output=None):
|
|
861
|
+
"""
|
|
862
|
+
method to add more runs to a timeseries dataframe with the same input
|
|
863
|
+
parameters as the original timeseries.
|
|
864
|
+
|
|
865
|
+
:param timeseries_df:
|
|
866
|
+
:param total_runs:
|
|
867
|
+
:param dir_output:
|
|
868
|
+
:return:
|
|
869
|
+
"""
|
|
870
|
+
added_runs = total_runs - 1
|
|
871
|
+
|
|
872
|
+
s_step = int(timeseries_df.index.freqstr[:-1])
|
|
873
|
+
mean_drawoff_vol_per_day = timeseries_df['mean_drawoff_vol_per_day'][0]
|
|
874
|
+
weekend_weekday_factor = timeseries_df['weekend_weekday_factor'][0]
|
|
875
|
+
initial_day = timeseries_df['initial_day'][0]
|
|
876
|
+
method = timeseries_df['method'][0]
|
|
877
|
+
categories = timeseries_df['categories'][0]
|
|
878
|
+
|
|
879
|
+
if method == 'OpenDHW':
|
|
880
|
+
|
|
881
|
+
for run in range(added_runs):
|
|
882
|
+
extra_timeseries_df = generate_dhw_profile(
|
|
883
|
+
s_step=s_step,
|
|
884
|
+
categories=categories,
|
|
885
|
+
holidays=holidays,
|
|
886
|
+
weekend_weekday_factor=weekend_weekday_factor,
|
|
887
|
+
mean_drawoff_vol_per_day=mean_drawoff_vol_per_day,
|
|
888
|
+
initial_day=initial_day
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
additional_profile = extra_timeseries_df['Water_LperH']
|
|
892
|
+
timeseries_df['Water_LperH_' + str(run)] = additional_profile
|
|
893
|
+
|
|
894
|
+
elif method == 'DHWcalc':
|
|
895
|
+
|
|
896
|
+
raise Exception('adding multiple plots for DWHcalc is not so useful, '
|
|
897
|
+
'as DHWcalc does not work with a random seed!')
|
|
898
|
+
|
|
899
|
+
if dir_output is not None:
|
|
900
|
+
# set a name for the file
|
|
901
|
+
save_name = "{}_{}runs_{}L_{}min.csv".format(
|
|
902
|
+
method, total_runs, mean_drawoff_vol_per_day, int(s_step / 60))
|
|
903
|
+
|
|
904
|
+
# make a directory. if it already exists, no problem, just use it
|
|
905
|
+
dir_output.mkdir(exist_ok=True)
|
|
906
|
+
|
|
907
|
+
# save the dataframe in the folder as a csv with the chosen name
|
|
908
|
+
timeseries_df.to_csv(dir_output / save_name)
|
|
909
|
+
|
|
910
|
+
return timeseries_df
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def get_drawoffs(timeseries_df, remove_cats=True):
|
|
914
|
+
"""
|
|
915
|
+
get sorted drawoff events from a timeseries Dataframe.
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
# only columns that contain 'Water_LperH'
|
|
919
|
+
timeseries_df.columns = timeseries_df.columns.astype(str)
|
|
920
|
+
cols_bool_str = timeseries_df.columns.str.contains('Water_LperH')
|
|
921
|
+
water_LperH_df = timeseries_df.loc[:, cols_bool_str]
|
|
922
|
+
|
|
923
|
+
if remove_cats:
|
|
924
|
+
# not columns that contain 'cat'
|
|
925
|
+
cols_bool_str2 = water_LperH_df.columns.str.contains('cat')
|
|
926
|
+
cols_bool_str2 = [not i for i in cols_bool_str2]
|
|
927
|
+
water_LperH_df = water_LperH_df.loc[:, cols_bool_str2]
|
|
928
|
+
|
|
929
|
+
drawoffs_df = water_LperH_df.reset_index(drop=True)
|
|
930
|
+
|
|
931
|
+
for col_name in drawoffs_df.columns:
|
|
932
|
+
# From each column, get only values != 0.
|
|
933
|
+
drawoffs_series = water_LperH_df[water_LperH_df[col_name] != 0][
|
|
934
|
+
col_name]
|
|
935
|
+
drawoffs_lst = list(drawoffs_series)
|
|
936
|
+
|
|
937
|
+
# fill zero-values with NaN's
|
|
938
|
+
empty_cells_len = len(timeseries_df) - len(drawoffs_lst)
|
|
939
|
+
empty_cells_lst = [np.nan] * empty_cells_len
|
|
940
|
+
drawoffs_lst.extend(empty_cells_lst)
|
|
941
|
+
drawoffs_lst.sort()
|
|
942
|
+
|
|
943
|
+
# append to the drawoff dataframe
|
|
944
|
+
drawoffs_df[col_name] = drawoffs_lst
|
|
945
|
+
|
|
946
|
+
# Drop rows that have only NaN's as values
|
|
947
|
+
drawoffs_df = drawoffs_df.dropna(how='all')
|
|
948
|
+
|
|
949
|
+
return drawoffs_df
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def plot_multiple_runs(timeseries_df, plot_demands_overlay=True,
|
|
953
|
+
start_plot='2019-02-01', end_plot='2019-02-02',
|
|
954
|
+
plot_hist=True, plot_kde=True):
|
|
955
|
+
"""
|
|
956
|
+
This function should only be used when the 'add_additional_runs' function
|
|
957
|
+
has been used before.
|
|
958
|
+
|
|
959
|
+
:param timeseries_df: df: dataframe with timesieries
|
|
960
|
+
:param plot_demands_overlay: bool: plot lineplot
|
|
961
|
+
:param start_plot: str: start date
|
|
962
|
+
:param end_plot: str: end date
|
|
963
|
+
:param plot_hist: bool: plot histogram
|
|
964
|
+
:param plot_kde: bool: plot kde plot
|
|
965
|
+
"""
|
|
966
|
+
|
|
967
|
+
drawoffs_df = get_drawoffs(timeseries_df=timeseries_df)
|
|
968
|
+
|
|
969
|
+
if plot_demands_overlay:
|
|
970
|
+
fig, ax1 = plt.subplots()
|
|
971
|
+
fig.tight_layout()
|
|
972
|
+
|
|
973
|
+
# only columns that contain 'Water_LperH'
|
|
974
|
+
cols_bool_str = timeseries_df.columns.str.contains('Water_LperH')
|
|
975
|
+
water_LperH_df = timeseries_df.loc[:, cols_bool_str]
|
|
976
|
+
|
|
977
|
+
# not columns that contrain 'cats'
|
|
978
|
+
cols_bool_str2 = water_LperH_df.columns.str.contains('cat')
|
|
979
|
+
cols_bool_str2 = [not i for i in cols_bool_str2]
|
|
980
|
+
water_LperH_df = water_LperH_df.loc[:, cols_bool_str2]
|
|
981
|
+
|
|
982
|
+
ax1 = sns.lineplot(ax=ax1, data=water_LperH_df[start_plot:end_plot],
|
|
983
|
+
linewidth=0.5, legend=False)
|
|
984
|
+
|
|
985
|
+
# set beautiful x axis ticks for datetime
|
|
986
|
+
# https://matplotlib.org/3.1.1/gallery/ticks_and_spines/date_concise_formatter.html
|
|
987
|
+
locator = mdates.AutoDateLocator()
|
|
988
|
+
formatter = mdates.ConciseDateFormatter(locator)
|
|
989
|
+
ax1.xaxis.set_major_locator(locator)
|
|
990
|
+
ax1.xaxis.set_major_formatter(formatter)
|
|
991
|
+
|
|
992
|
+
plt.show()
|
|
993
|
+
|
|
994
|
+
if plot_hist:
|
|
995
|
+
sns.histplot(data=drawoffs_df, kde=False, element="step", fill=False,
|
|
996
|
+
stat='count', line_kws={'alpha': 0.8, 'linewidth': 0.9})
|
|
997
|
+
|
|
998
|
+
title_str = make_title_str(timeseries_df)
|
|
999
|
+
plt.title(title_str)
|
|
1000
|
+
|
|
1001
|
+
plt.show()
|
|
1002
|
+
|
|
1003
|
+
if plot_kde:
|
|
1004
|
+
sns.kdeplot(data=drawoffs_df, bw_adjust=0.1, alpha=0.5, fill=False,
|
|
1005
|
+
linewidth=0.5, legend=True)
|
|
1006
|
+
|
|
1007
|
+
title_str = make_title_str(timeseries_df)
|
|
1008
|
+
plt.title(title_str)
|
|
1009
|
+
|
|
1010
|
+
plt.show()
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def plot_multiple_timeseries(timeseries_lst, col_part='Water_LperH',
|
|
1014
|
+
plot_demands_overlay=True,
|
|
1015
|
+
start_plot='2019-02-01', end_plot='2019-02-02',
|
|
1016
|
+
plot_hist=True, plot_kde=True):
|
|
1017
|
+
"""
|
|
1018
|
+
plots multiple timeseries given in a list. better than "plot multiple runs?"
|
|
1019
|
+
|
|
1020
|
+
:param timeseries_lst: list: list with timeseries dataframes
|
|
1021
|
+
:param col_part: str: string that matches colum names
|
|
1022
|
+
which should be plotted
|
|
1023
|
+
:param plot_demands_overlay: bool: plot lineplot of all dfs
|
|
1024
|
+
:param start_plot: str: start of lineplot
|
|
1025
|
+
:param end_plot: str: end of lineplot
|
|
1026
|
+
:param plot_hist: bool: plot histogram
|
|
1027
|
+
:param plot_kde: bool: plot kde plot
|
|
1028
|
+
:return:
|
|
1029
|
+
"""
|
|
1030
|
+
|
|
1031
|
+
# get the index column of one timeseries and use it to make a plot df.
|
|
1032
|
+
plot_index = timeseries_lst[0].index
|
|
1033
|
+
plot_df = pd.DataFrame(index=plot_index)
|
|
1034
|
+
|
|
1035
|
+
for i, df in enumerate(timeseries_lst):
|
|
1036
|
+
# get colum names
|
|
1037
|
+
cols_LperH = [name for name in list(df.columns) if col_part in name]
|
|
1038
|
+
|
|
1039
|
+
# the timeseries_df should only have 1 column that matches the
|
|
1040
|
+
# desired string. more are not implemented yet
|
|
1041
|
+
assert len(cols_LperH) <= 1
|
|
1042
|
+
|
|
1043
|
+
# fill the plot dataframe with the matching column
|
|
1044
|
+
plot_df[i] = df[cols_LperH]
|
|
1045
|
+
|
|
1046
|
+
drawoffs_df = get_drawoffs(timeseries_df=plot_df)
|
|
1047
|
+
|
|
1048
|
+
if plot_demands_overlay:
|
|
1049
|
+
fig, ax1 = plt.subplots()
|
|
1050
|
+
fig.tight_layout()
|
|
1051
|
+
|
|
1052
|
+
ax1 = sns.lineplot(ax=ax1, data=plot_df[start_plot:end_plot],
|
|
1053
|
+
linewidth=0.5, legend=True)
|
|
1054
|
+
|
|
1055
|
+
# set beautiful x axis ticks for datetime
|
|
1056
|
+
# https://matplotlib.org/3.1.1/gallery/ticks_and_spines/date_concise_formatter.html
|
|
1057
|
+
locator = mdates.AutoDateLocator()
|
|
1058
|
+
formatter = mdates.ConciseDateFormatter(locator)
|
|
1059
|
+
ax1.xaxis.set_major_locator(locator)
|
|
1060
|
+
ax1.xaxis.set_major_formatter(formatter)
|
|
1061
|
+
|
|
1062
|
+
plt.show()
|
|
1063
|
+
|
|
1064
|
+
if plot_hist:
|
|
1065
|
+
sns.histplot(data=drawoffs_df, kde=True, element="step", fill=False,
|
|
1066
|
+
stat='count', line_kws={'alpha': 0.8, 'linewidth': 0.9})
|
|
1067
|
+
plt.show()
|
|
1068
|
+
|
|
1069
|
+
if plot_kde:
|
|
1070
|
+
sns.kdeplot(data=drawoffs_df, bw_adjust=0.1, alpha=0.5, fill=False,
|
|
1071
|
+
linewidth=0.5, legend=True)
|
|
1072
|
+
plt.show()
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def compare_generators(timeseries_df_1, timeseries_df_2,
|
|
1076
|
+
start_plot='2019-03-01', end_plot='2019-03-08',
|
|
1077
|
+
plot_date_slice=True, plot_distribution=True,
|
|
1078
|
+
plot_detailed_distribution=True, save_fig=False):
|
|
1079
|
+
"""
|
|
1080
|
+
Compares two timeseries by plotting them next to each other with the same
|
|
1081
|
+
x and y axis limits.
|
|
1082
|
+
|
|
1083
|
+
:param timeseries_df_1: df: first timeseries dataframe
|
|
1084
|
+
:param timeseries_df_2: df: second timeseries dataframe
|
|
1085
|
+
:param start_plot: str: date, f.e. 2019-03-01
|
|
1086
|
+
:param end_plot: str: date, f.e. 2019-03-08
|
|
1087
|
+
:param plot_date_slice: bool: plot lineplots
|
|
1088
|
+
:param plot_distribution: bool: plot histplots
|
|
1089
|
+
:param plot_detailed_distribution: bool: plot detailed histplots
|
|
1090
|
+
:param save_fig: bool: save the plot
|
|
1091
|
+
"""
|
|
1092
|
+
|
|
1093
|
+
cats_1 = timeseries_df_1['categories'][0]
|
|
1094
|
+
cats_2 = timeseries_df_2['categories'][0]
|
|
1095
|
+
if cats_1 or cats_2 == 1:
|
|
1096
|
+
print("detailed distribution is designed to compare timeseries with "
|
|
1097
|
+
"one drawoff category")
|
|
1098
|
+
plot_detailed_distribution = False
|
|
1099
|
+
|
|
1100
|
+
# compute Stats for the title
|
|
1101
|
+
drawoffs_1 = timeseries_df_1[timeseries_df_1['Water_LperH'] != 0][
|
|
1102
|
+
'Water_LperH']
|
|
1103
|
+
|
|
1104
|
+
drawoffs_2 = timeseries_df_2[timeseries_df_2['Water_LperH'] != 0][
|
|
1105
|
+
'Water_LperH']
|
|
1106
|
+
|
|
1107
|
+
if plot_date_slice:
|
|
1108
|
+
|
|
1109
|
+
# make dataframe for plotting with seaborn
|
|
1110
|
+
plot_df_1 = timeseries_df_1[['Water_LperH', 'mean_drawoff_vol_per_day']]
|
|
1111
|
+
plot_df_2 = timeseries_df_2[['Water_LperH', 'mean_drawoff_vol_per_day']]
|
|
1112
|
+
|
|
1113
|
+
fig, (ax1, ax2) = plt.subplots(2, 1)
|
|
1114
|
+
fig.tight_layout()
|
|
1115
|
+
|
|
1116
|
+
# First Subplot
|
|
1117
|
+
ax1 = sns.lineplot(ax=ax1, data=plot_df_1[start_plot:end_plot],
|
|
1118
|
+
linewidth=1.0, palette=[rwth_blue, rwth_red])
|
|
1119
|
+
|
|
1120
|
+
title_str_1 = make_title_str(timeseries_df=timeseries_df_1)
|
|
1121
|
+
ax1.set_title(title_str_1)
|
|
1122
|
+
|
|
1123
|
+
ax1.legend(loc="upper left")
|
|
1124
|
+
|
|
1125
|
+
# Second Subplot
|
|
1126
|
+
ax2 = sns.lineplot(ax=ax2, data=plot_df_2[start_plot:end_plot],
|
|
1127
|
+
linewidth=1.0, palette=[rwth_blue, rwth_red])
|
|
1128
|
+
|
|
1129
|
+
title_str_2 = make_title_str(timeseries_df=timeseries_df_2)
|
|
1130
|
+
ax2.set_title(title_str_2)
|
|
1131
|
+
|
|
1132
|
+
ax2.legend(loc="upper left")
|
|
1133
|
+
|
|
1134
|
+
# --- set both aes to the same y limit ---
|
|
1135
|
+
ymin1, ymax1 = ax1.get_ylim()
|
|
1136
|
+
ymin2, ymax2 = ax2.get_ylim()
|
|
1137
|
+
|
|
1138
|
+
ymax_set = max(ymax1, ymax2)
|
|
1139
|
+
|
|
1140
|
+
ax1.set_ylim(ymin1, ymax_set)
|
|
1141
|
+
ax2.set_ylim(ymin2, ymax_set)
|
|
1142
|
+
|
|
1143
|
+
# --- beautiful x-ticks ---
|
|
1144
|
+
locator = mdates.AutoDateLocator()
|
|
1145
|
+
formatter = mdates.ConciseDateFormatter(locator)
|
|
1146
|
+
ax1.xaxis.set_major_locator(locator)
|
|
1147
|
+
ax1.xaxis.set_major_formatter(formatter)
|
|
1148
|
+
ax2.xaxis.set_major_locator(locator)
|
|
1149
|
+
ax2.xaxis.set_major_formatter(formatter)
|
|
1150
|
+
|
|
1151
|
+
plt.show()
|
|
1152
|
+
|
|
1153
|
+
if save_fig:
|
|
1154
|
+
dir_output = Path.cwd() / "plots"
|
|
1155
|
+
dir_output.mkdir(exist_ok=True)
|
|
1156
|
+
|
|
1157
|
+
fname = "Timeseries_Comparison_Lineplot"
|
|
1158
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
1159
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
1160
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
1161
|
+
|
|
1162
|
+
if plot_distribution:
|
|
1163
|
+
# compute Jensen Shannon Distance
|
|
1164
|
+
distance = jensen_shannon_distance(q=timeseries_df_1['Water_LperH'],
|
|
1165
|
+
p=timeseries_df_2['Water_LperH'])
|
|
1166
|
+
|
|
1167
|
+
fig, (ax1, ax2) = plt.subplots(2, 1)
|
|
1168
|
+
fig.tight_layout()
|
|
1169
|
+
|
|
1170
|
+
# plot the distribution
|
|
1171
|
+
# https://seaborn.pydata.org/generated/seaborn.displot.html
|
|
1172
|
+
ax1 = sns.histplot(ax=ax1, data=drawoffs_1, kde=True)
|
|
1173
|
+
ax2 = sns.histplot(ax=ax2, data=drawoffs_2, kde=True)
|
|
1174
|
+
|
|
1175
|
+
# --- Set titles and Labels ---
|
|
1176
|
+
title_str_1 = make_title_str(timeseries_df=timeseries_df_1)
|
|
1177
|
+
title_str_1 = 'Jensen Shannon Distance = {:.4f} \n'.format(distance) \
|
|
1178
|
+
+ title_str_1
|
|
1179
|
+
ax1.set_title(title_str_1)
|
|
1180
|
+
|
|
1181
|
+
ax1.set_ylabel('Count in a Year')
|
|
1182
|
+
ax1.set_xlabel('Flowrate [L/h]')
|
|
1183
|
+
|
|
1184
|
+
title_str_2 = make_title_str(timeseries_df=timeseries_df_2)
|
|
1185
|
+
ax2.set_title(title_str_2)
|
|
1186
|
+
|
|
1187
|
+
ax2.set_ylabel('Count in a Year')
|
|
1188
|
+
ax2.set_xlabel('Flowrate [L/h]')
|
|
1189
|
+
|
|
1190
|
+
# --- set both axes to the same y limit ---
|
|
1191
|
+
ymin1, ymax1 = ax1.get_ylim()
|
|
1192
|
+
ymin2, ymax2 = ax2.get_ylim()
|
|
1193
|
+
|
|
1194
|
+
ymax_set = max(ymax1, ymax2)
|
|
1195
|
+
|
|
1196
|
+
ax1.set_ylim(ymin1, ymax_set)
|
|
1197
|
+
ax2.set_ylim(ymin2, ymax_set)
|
|
1198
|
+
|
|
1199
|
+
# --- set both axes to the same x limit ---
|
|
1200
|
+
xmin1, xmax1 = ax1.get_xlim()
|
|
1201
|
+
xmin2, xmax2 = ax2.get_xlim()
|
|
1202
|
+
|
|
1203
|
+
xmax_set = max(xmax1, xmax2)
|
|
1204
|
+
|
|
1205
|
+
ax1.set_xlim(xmin1, xmax_set)
|
|
1206
|
+
ax2.set_xlim(xmin2, xmax_set)
|
|
1207
|
+
|
|
1208
|
+
plt.show()
|
|
1209
|
+
|
|
1210
|
+
if save_fig:
|
|
1211
|
+
dir_output = Path.cwd() / "plots"
|
|
1212
|
+
dir_output.mkdir(exist_ok=True)
|
|
1213
|
+
|
|
1214
|
+
fname = "Timeseries_Comparison_Histplot"
|
|
1215
|
+
fig.savefig(dir_output / (fname + '.pdf'))
|
|
1216
|
+
fig.savefig(dir_output / (fname + '.svg'))
|
|
1217
|
+
fig.savefig(dir_output / (fname + '.png'))
|
|
1218
|
+
|
|
1219
|
+
if plot_detailed_distribution:
|
|
1220
|
+
|
|
1221
|
+
# https://towardsdatascience.com/advanced-histogram-using-python-bceae288e715
|
|
1222
|
+
|
|
1223
|
+
# compute Jensen Shannon Distance
|
|
1224
|
+
distance = jensen_shannon_distance(q=timeseries_df_1['Water_LperH'],
|
|
1225
|
+
p=timeseries_df_2['Water_LperH'])
|
|
1226
|
+
|
|
1227
|
+
fig, axes = plt.subplots(2, 1)
|
|
1228
|
+
ax1 = axes[0]
|
|
1229
|
+
ax2 = axes[1]
|
|
1230
|
+
fig.tight_layout()
|
|
1231
|
+
|
|
1232
|
+
drawoffs_lst = [drawoffs_1, drawoffs_2]
|
|
1233
|
+
|
|
1234
|
+
# create bin values
|
|
1235
|
+
mean1 = timeseries_df_1['mean_drawoff_flow_rate_LperH'][0]
|
|
1236
|
+
sdtdev1 = timeseries_df_1['sdtdev_drawoff_flow_rate_LperH'][0]
|
|
1237
|
+
non_zero_min1 = timeseries_df_1[timeseries_df_1['Water_LperH'] > 0][
|
|
1238
|
+
'Water_LperH'].min() # smallest entry that is not 0.
|
|
1239
|
+
|
|
1240
|
+
bin_values1 = [non_zero_min1,
|
|
1241
|
+
mean1 - 2 * sdtdev1,
|
|
1242
|
+
mean1 - sdtdev1,
|
|
1243
|
+
mean1,
|
|
1244
|
+
mean1 + sdtdev1,
|
|
1245
|
+
mean1 + 2 * sdtdev1,
|
|
1246
|
+
timeseries_df_1['Water_LperH'].max()]
|
|
1247
|
+
bin_values1 = list(set(bin_values1)) # remove double entries
|
|
1248
|
+
bin_values1.sort() # bins have to be sorted
|
|
1249
|
+
|
|
1250
|
+
mean2 = timeseries_df_2['mean_drawoff_flow_rate_LperH'][0]
|
|
1251
|
+
sdtdev2 = timeseries_df_2['sdtdev_drawoff_flow_rate_LperH'][0]
|
|
1252
|
+
non_zero_min2 = timeseries_df_2[timeseries_df_2['Water_LperH'] > 0][
|
|
1253
|
+
'Water_LperH'].min() # smallest entry that is not 0.
|
|
1254
|
+
|
|
1255
|
+
bin_values2 = [non_zero_min2,
|
|
1256
|
+
mean2 - 2 * sdtdev2,
|
|
1257
|
+
mean2 - sdtdev2,
|
|
1258
|
+
mean2,
|
|
1259
|
+
mean2 + sdtdev2,
|
|
1260
|
+
mean2 + 2 * sdtdev2,
|
|
1261
|
+
timeseries_df_2['Water_LperH'].max()]
|
|
1262
|
+
bin_values2 = list(set(bin_values2)) # remove double entries
|
|
1263
|
+
bin_values2.sort() # bins have to be sorted
|
|
1264
|
+
|
|
1265
|
+
bin_values_lst = [bin_values1, bin_values2]
|
|
1266
|
+
|
|
1267
|
+
for sub_i, drawoffs_i in enumerate(drawoffs_lst):
|
|
1268
|
+
|
|
1269
|
+
ax = axes[sub_i]
|
|
1270
|
+
|
|
1271
|
+
counts, bins, patches = ax.hist(
|
|
1272
|
+
drawoffs_i, bins=bin_values_lst[sub_i], edgecolor='black')
|
|
1273
|
+
|
|
1274
|
+
# Set the ticks to be at the edges of the bins.
|
|
1275
|
+
ax.set_xticks(bins.round(2))
|
|
1276
|
+
|
|
1277
|
+
# Calculate bar centre to display the count of data points and %
|
|
1278
|
+
bin_x_centers = 0.1 * np.diff(bins) + bins[:-1]
|
|
1279
|
+
bin_y_centers = ax.get_yticks()[1] * 0.25
|
|
1280
|
+
|
|
1281
|
+
# Display the the count of data points and % for each bar in hist
|
|
1282
|
+
for i in range(len(bins) - 1):
|
|
1283
|
+
bin_label = str(int(counts[i])) + "\n{0:.2f}%".format(
|
|
1284
|
+
(counts[i] / counts.sum()) * 100)
|
|
1285
|
+
ax.text(bin_x_centers[i], bin_y_centers, bin_label, rotation=0)
|
|
1286
|
+
|
|
1287
|
+
title_str_1 = make_title_str(timeseries_df=timeseries_df_1)
|
|
1288
|
+
title_str_1 = 'Jensen Shannon Distance = {:.4f} \n'.format(distance) \
|
|
1289
|
+
+ title_str_1
|
|
1290
|
+
ax1.set_title(title_str_1)
|
|
1291
|
+
|
|
1292
|
+
ax1.set_ylabel('Count in a Year')
|
|
1293
|
+
|
|
1294
|
+
title_str_2 = make_title_str(timeseries_df=timeseries_df_2)
|
|
1295
|
+
ax2.set_title(title_str_2)
|
|
1296
|
+
|
|
1297
|
+
ax2.set_ylabel('Count in a Year')
|
|
1298
|
+
ax2.set_xlabel('Flowrate [L/h]')
|
|
1299
|
+
|
|
1300
|
+
# --- set both aes to the same y limit ---
|
|
1301
|
+
ymin1, ymax1 = ax1.get_ylim()
|
|
1302
|
+
ymin2, ymax2 = ax2.get_ylim()
|
|
1303
|
+
|
|
1304
|
+
ymax_set = max(ymax1, ymax2)
|
|
1305
|
+
|
|
1306
|
+
ax1.set_ylim(ymin1, ymax_set)
|
|
1307
|
+
ax2.set_ylim(ymin2, ymax_set)
|
|
1308
|
+
|
|
1309
|
+
plt.show()
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def plot_three_histplots(timeseries_df_1, timeseries_df_2, timeseries_df_3):
|
|
1313
|
+
"""
|
|
1314
|
+
Compares three timeseries by means of a triple subplot.
|
|
1315
|
+
:param timeseries_df_1: df: first time series
|
|
1316
|
+
:param timeseries_df_2: df: second time series
|
|
1317
|
+
:param timeseries_df_3: df: third time series
|
|
1318
|
+
|
|
1319
|
+
"""
|
|
1320
|
+
|
|
1321
|
+
# compute Stats for the title
|
|
1322
|
+
drawoffs_1 = timeseries_df_1[timeseries_df_1['Water_LperH'] != 0][
|
|
1323
|
+
'Water_LperH']
|
|
1324
|
+
drawoffs_2 = timeseries_df_2[timeseries_df_2['Water_LperH'] != 0][
|
|
1325
|
+
'Water_LperH']
|
|
1326
|
+
drawoffs_3 = timeseries_df_3[timeseries_df_3['Water_LperH'] != 0][
|
|
1327
|
+
'Water_LperH']
|
|
1328
|
+
|
|
1329
|
+
fig, (ax1, ax2, ax3) = plt.subplots(3, 1)
|
|
1330
|
+
fig.tight_layout()
|
|
1331
|
+
|
|
1332
|
+
# plot the distribution
|
|
1333
|
+
# https://seaborn.pydata.org/generated/seaborn.displot.html
|
|
1334
|
+
ax1 = sns.histplot(ax=ax1, data=drawoffs_1, kde=True)
|
|
1335
|
+
ax2 = sns.histplot(ax=ax2, data=drawoffs_2, kde=True)
|
|
1336
|
+
ax3 = sns.histplot(ax=ax3, data=drawoffs_3, kde=True)
|
|
1337
|
+
|
|
1338
|
+
# --- Set titles and Labels ---
|
|
1339
|
+
title_str_1 = make_title_str(timeseries_df=timeseries_df_1)
|
|
1340
|
+
ax1.set_title(title_str_1)
|
|
1341
|
+
ax1.set_ylabel('Count in a Year')
|
|
1342
|
+
ax1.set_xlabel('Flowrate [L/h]')
|
|
1343
|
+
|
|
1344
|
+
title_str_2 = make_title_str(timeseries_df=timeseries_df_2)
|
|
1345
|
+
ax2.set_title(title_str_2)
|
|
1346
|
+
ax2.set_ylabel('Count in a Year')
|
|
1347
|
+
ax2.set_xlabel('Flowrate [L/h]')
|
|
1348
|
+
|
|
1349
|
+
title_str_3 = make_title_str(timeseries_df=timeseries_df_3)
|
|
1350
|
+
ax3.set_title(title_str_3)
|
|
1351
|
+
ax3.set_ylabel('Count in a Year')
|
|
1352
|
+
ax3.set_xlabel('Flowrate [L/h]')
|
|
1353
|
+
|
|
1354
|
+
# --- set both axes to the same y limit ---
|
|
1355
|
+
ymin1, ymax1 = ax1.get_ylim()
|
|
1356
|
+
ymin2, ymax2 = ax2.get_ylim()
|
|
1357
|
+
ymin3, ymax3 = ax3.get_ylim()
|
|
1358
|
+
|
|
1359
|
+
ymax_set = max(ymax1, ymax2, ymax3)
|
|
1360
|
+
|
|
1361
|
+
ax1.set_ylim(ymin1, ymax_set)
|
|
1362
|
+
ax2.set_ylim(ymin2, ymax_set)
|
|
1363
|
+
ax3.set_ylim(ymin3, ymax_set)
|
|
1364
|
+
|
|
1365
|
+
# --- set both axes to the same x limit ---
|
|
1366
|
+
xmin1, xmax1 = ax1.get_xlim()
|
|
1367
|
+
xmin2, xmax2 = ax2.get_xlim()
|
|
1368
|
+
xmin3, xmax3 = ax3.get_xlim()
|
|
1369
|
+
|
|
1370
|
+
xmax_set = max(xmax1, xmax2, xmax3)
|
|
1371
|
+
|
|
1372
|
+
ax1.set_xlim(xmin1, xmax_set)
|
|
1373
|
+
ax2.set_xlim(xmin2, xmax_set)
|
|
1374
|
+
ax3.set_xlim(xmin3, xmax_set)
|
|
1375
|
+
|
|
1376
|
+
plt.show()
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def jensen_shannon_distance(p, q):
|
|
1380
|
+
"""
|
|
1381
|
+
method to compute the Jenson-Shannon Distance between two probability
|
|
1382
|
+
distributions. 0 indicates that the two distributions are the same,
|
|
1383
|
+
and 1 would indicate that they are nowhere similar.
|
|
1384
|
+
|
|
1385
|
+
From https://medium.com/@sourcedexter/how-to-find-the-similarity-between-two-probability-distributions-using-python-a7546e90a08d
|
|
1386
|
+
"""
|
|
1387
|
+
|
|
1388
|
+
# convert the vectors into numpy arrays in case that they aren't
|
|
1389
|
+
p = np.array(p)
|
|
1390
|
+
q = np.array(q)
|
|
1391
|
+
|
|
1392
|
+
# calculate m
|
|
1393
|
+
m = (p + q) / 2
|
|
1394
|
+
|
|
1395
|
+
# compute Jensen Shannon Divergence
|
|
1396
|
+
divergence = (scipy.stats.entropy(p, m) + scipy.stats.entropy(q, m)) / 2
|
|
1397
|
+
|
|
1398
|
+
# compute the Jensen Shannon Distance
|
|
1399
|
+
distance = np.sqrt(divergence)
|
|
1400
|
+
|
|
1401
|
+
return round(distance, 4)
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def get_s_step(timeseries_df):
|
|
1405
|
+
"""
|
|
1406
|
+
get the seconds within a timestep from a pandas dataframe. When loading
|
|
1407
|
+
Dataframes from a csv, the index loses its 'freq' attribute. This is thus
|
|
1408
|
+
just a workaround when loading Timeseries from csv.
|
|
1409
|
+
"""
|
|
1410
|
+
|
|
1411
|
+
try:
|
|
1412
|
+
s_step = int(timeseries_df.index.freqstr[:-1])
|
|
1413
|
+
# todo: why doesnt this work for Dataframes loaded from a csv?
|
|
1414
|
+
|
|
1415
|
+
except TypeError:
|
|
1416
|
+
|
|
1417
|
+
steps = len(timeseries_df)
|
|
1418
|
+
secs_in_year = 8760 * 60 * 60
|
|
1419
|
+
s_step = secs_in_year / steps
|
|
1420
|
+
|
|
1421
|
+
# check if s_step has no decimal points (should not be 60.01 f.e.)
|
|
1422
|
+
assert s_step % 1 == 0
|
|
1423
|
+
s_step = int(s_step)
|
|
1424
|
+
|
|
1425
|
+
return s_step
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def make_title_str(timeseries_df):
|
|
1429
|
+
"""
|
|
1430
|
+
creates a title string based on the timeseries dataframe. The title
|
|
1431
|
+
string can then be used for a variety of plots.
|
|
1432
|
+
"""
|
|
1433
|
+
|
|
1434
|
+
# compute additional stats for title
|
|
1435
|
+
s_step = get_s_step(timeseries_df)
|
|
1436
|
+
yearly_water_demand = timeseries_df['Water_L'].sum() # in L
|
|
1437
|
+
drawoffs = timeseries_df[timeseries_df['Water_LperH'] != 0]['Water_LperH']
|
|
1438
|
+
max_water_flow = timeseries_df['Water_LperH'].max()
|
|
1439
|
+
method = timeseries_df['method'][0]
|
|
1440
|
+
cats = timeseries_df['categories'][0]
|
|
1441
|
+
|
|
1442
|
+
if cats == 1:
|
|
1443
|
+
method = "{} ({} cat)".format(method, cats)
|
|
1444
|
+
|
|
1445
|
+
title_str = '{}, ∆t = {}, Yearly Demand = {:.1f} L \n' \
|
|
1446
|
+
'No. Drawoffs = {}, Peak = {:.1f} L/h, ' \
|
|
1447
|
+
'Mean = {:.1f} L/h, SdtDev = {:.1f} L/h'.format(
|
|
1448
|
+
method, s_step, yearly_water_demand, len(drawoffs), max_water_flow,
|
|
1449
|
+
drawoffs.mean(), drawoffs.std())
|
|
1450
|
+
|
|
1451
|
+
else: # f.e. four categories
|
|
1452
|
+
|
|
1453
|
+
if 'OpenDHW' in method:
|
|
1454
|
+
|
|
1455
|
+
method = "{} ({} cats)".format(method, cats)
|
|
1456
|
+
|
|
1457
|
+
col_names = list(timeseries_df.columns)
|
|
1458
|
+
cols_LperH = [name for name in col_names if 'Water_L_' in name]
|
|
1459
|
+
water_LperH_df = timeseries_df[cols_LperH]
|
|
1460
|
+
|
|
1461
|
+
cats_str = ''
|
|
1462
|
+
for col in cols_LperH:
|
|
1463
|
+
cat_sum = water_LperH_df[col].sum()
|
|
1464
|
+
cats_str += '{:.0f} L, '.format(cat_sum)
|
|
1465
|
+
cats_str = cats_str[:-2]
|
|
1466
|
+
|
|
1467
|
+
title_str = f'{method}, ∆t = {s_step}, No. Drawoffs =' \
|
|
1468
|
+
f' {len(drawoffs)}, Peak = {max_water_flow:.1f} L/h ' \
|
|
1469
|
+
f'\n Yearly Demand = {yearly_water_demand:.0f} L (=' \
|
|
1470
|
+
f' {cats_str})'
|
|
1471
|
+
|
|
1472
|
+
elif 'DHWcalc' in method:
|
|
1473
|
+
|
|
1474
|
+
method = "{} ({} cats)".format(method, cats)
|
|
1475
|
+
|
|
1476
|
+
title_str = f"{method}, ∆t = {s_step}, No. Drawoffs =" \
|
|
1477
|
+
f" {len(drawoffs)}, Peak = {max_water_flow:.1f} L/h " \
|
|
1478
|
+
f"\n Yearly Demand = {yearly_water_demand:.0f} L"
|
|
1479
|
+
|
|
1480
|
+
else:
|
|
1481
|
+
raise Exception("Unkown method, try 'OpenDHW' or 'DHWcalc'.")
|
|
1482
|
+
|
|
1483
|
+
return title_str
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def resample_water_series(timeseries_df, s_step_output):
|
|
1487
|
+
"""
|
|
1488
|
+
Before resampling a dataframe, we have to choose which data has to be
|
|
1489
|
+
resampled in what way. some columns list constants, some list intensive
|
|
1490
|
+
properties (like L/h, kW) and some list extensive properties (Like
|
|
1491
|
+
Liters/kWh).
|
|
1492
|
+
Constants should stay the same, intensive properties should be averaged
|
|
1493
|
+
and extensive properties should be summed up.
|
|
1494
|
+
|
|
1495
|
+
:param timeseries_df: df: dataframe that holds the timeseries
|
|
1496
|
+
:param s_step_output: int: desired output seconds in a timestep
|
|
1497
|
+
:return: timeseries_df_re: df: resampled dataframe
|
|
1498
|
+
"""
|
|
1499
|
+
|
|
1500
|
+
s_step_old = get_s_step(timeseries_df)
|
|
1501
|
+
conversion_factor = s_step_output / s_step_old
|
|
1502
|
+
|
|
1503
|
+
if conversion_factor != 1:
|
|
1504
|
+
# separate constants from variables
|
|
1505
|
+
cols_consts = list(timeseries_df.columns[timeseries_df.nunique() <= 1])
|
|
1506
|
+
cols_vars = list(timeseries_df.columns[timeseries_df.nunique() > 1])
|
|
1507
|
+
|
|
1508
|
+
# separate flows (intensive) from sums (extensive)
|
|
1509
|
+
cols_flows = [i for i in cols_vars if 'Lper' in i]
|
|
1510
|
+
cols_sums = [i for i in cols_vars if i not in cols_flows]
|
|
1511
|
+
|
|
1512
|
+
# separate constants that change with the timestep (intensive
|
|
1513
|
+
# properties)
|
|
1514
|
+
cols_const_flows = [i for i in cols_consts if 'Lper' in i]
|
|
1515
|
+
cols_consts = [i for i in cols_consts if i not in cols_const_flows]
|
|
1516
|
+
|
|
1517
|
+
# make new sub-dataframes
|
|
1518
|
+
timeseries_df_sum = timeseries_df[cols_sums]
|
|
1519
|
+
timeseries_df_flows = timeseries_df[cols_flows]
|
|
1520
|
+
timeseries_df_consts = timeseries_df[cols_consts]
|
|
1521
|
+
timeseries_df_const_flows = timeseries_df[cols_const_flows]
|
|
1522
|
+
|
|
1523
|
+
# resample them according to their physical properties
|
|
1524
|
+
rule = str(s_step_output) + 'S'
|
|
1525
|
+
timeseries_df_sum_re = timeseries_df_sum.resample(rule=rule).sum()
|
|
1526
|
+
timeseries_df_flows_re = timeseries_df_flows.resample(rule=rule).mean()
|
|
1527
|
+
|
|
1528
|
+
# cut the dataframe with the constant variables and update the index
|
|
1529
|
+
resampled_index = timeseries_df_sum_re.index
|
|
1530
|
+
timeseries_df_consts_cut = timeseries_df_consts[0:len(resampled_index)]
|
|
1531
|
+
timeseries_df_consts_re \
|
|
1532
|
+
= timeseries_df_consts_cut.set_index(resampled_index)
|
|
1533
|
+
|
|
1534
|
+
# update the constants that change with the timestep (intensive
|
|
1535
|
+
# properties) with the conversion factor
|
|
1536
|
+
timeseries_df_consts_flows_cut \
|
|
1537
|
+
= timeseries_df_const_flows[0:len(resampled_index)]
|
|
1538
|
+
timeseries_df_consts_flows_re \
|
|
1539
|
+
= timeseries_df_consts_flows_cut.set_index(resampled_index)
|
|
1540
|
+
timeseries_df_consts_flows_re \
|
|
1541
|
+
= timeseries_df_consts_flows_re / conversion_factor
|
|
1542
|
+
|
|
1543
|
+
timeseries_df_re = pd.concat(
|
|
1544
|
+
[timeseries_df_flows_re, timeseries_df_sum_re,
|
|
1545
|
+
timeseries_df_consts_re, timeseries_df_consts_flows_re],
|
|
1546
|
+
axis=1)
|
|
1547
|
+
|
|
1548
|
+
# add 'resampled' tag to method column
|
|
1549
|
+
timeseries_df_re['method'] = timeseries_df['method'][0] + ' (resampled)'
|
|
1550
|
+
|
|
1551
|
+
else:
|
|
1552
|
+
timeseries_df_re = timeseries_df
|
|
1553
|
+
|
|
1554
|
+
return timeseries_df_re
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def reduce_no_drawoffs(timeseries_df):
|
|
1558
|
+
"""
|
|
1559
|
+
for some reason, DHWcalc still yields less yearly drawoffs than OpenDHW.
|
|
1560
|
+
In case the yearly water demand is higher in an OpenDHW timeseries than
|
|
1561
|
+
the expected one, this function removes some randomly selected drawoffs
|
|
1562
|
+
events with a small flowrate to reduce the yearly water demand until its
|
|
1563
|
+
just under the expected one and simultaneously decreasing the number of
|
|
1564
|
+
drawoffs.
|
|
1565
|
+
|
|
1566
|
+
:param timeseries_df: df: input dataframe
|
|
1567
|
+
:return: timeseries_df_cleaned: df output dataframe
|
|
1568
|
+
"""
|
|
1569
|
+
|
|
1570
|
+
# get the expected yearly water demand
|
|
1571
|
+
expected_yearly_water = timeseries_df['mean_drawoff_vol_per_day'][0] * 365
|
|
1572
|
+
actual_yearly_water = timeseries_df['Water_L'].sum()
|
|
1573
|
+
|
|
1574
|
+
if expected_yearly_water < actual_yearly_water:
|
|
1575
|
+
|
|
1576
|
+
# select a cut off flow rate
|
|
1577
|
+
max_flow_rate = timeseries_df['Water_LperH'].max()
|
|
1578
|
+
min_flow_rate = \
|
|
1579
|
+
timeseries_df[timeseries_df['Water_LperH'] != 0].min()[
|
|
1580
|
+
'Water_LperH']
|
|
1581
|
+
cut_off_flow_rate = max(min_flow_rate * 5, max_flow_rate / 200)
|
|
1582
|
+
|
|
1583
|
+
# shuffle df so random days are selected when iterated over.
|
|
1584
|
+
timeseries_df_shuffled = timeseries_df.sample(frac=1).reset_index(
|
|
1585
|
+
drop=False)
|
|
1586
|
+
|
|
1587
|
+
# loop over the shuffled timeseries and set some vales to 0.
|
|
1588
|
+
for i in timeseries_df_shuffled.index:
|
|
1589
|
+
|
|
1590
|
+
curr_sum = timeseries_df_shuffled['Water_L'].sum()
|
|
1591
|
+
if curr_sum <= expected_yearly_water:
|
|
1592
|
+
break
|
|
1593
|
+
|
|
1594
|
+
curr_flow_rate = timeseries_df_shuffled.loc[i, 'Water_LperH']
|
|
1595
|
+
if curr_flow_rate != 0 and curr_flow_rate < cut_off_flow_rate:
|
|
1596
|
+
timeseries_df_shuffled.loc[i, 'Water_L'] = 0
|
|
1597
|
+
timeseries_df_shuffled.loc[i, 'Water_LperH'] = 0
|
|
1598
|
+
|
|
1599
|
+
# un-shuffle df
|
|
1600
|
+
timeseries_df_shuffled = timeseries_df_shuffled.set_index('index')
|
|
1601
|
+
timeseries_df_cleaned = timeseries_df_shuffled.sort_index()
|
|
1602
|
+
|
|
1603
|
+
else:
|
|
1604
|
+
timeseries_df_cleaned = timeseries_df
|
|
1605
|
+
print('No drawoffs have neen reduced, as expected_yearly_water >= '
|
|
1606
|
+
'actual_yearly_water')
|
|
1607
|
+
|
|
1608
|
+
return timeseries_df_cleaned
|