Skip to content

Commit 215c874

Browse files
committed
Move fitting of sigma to the simultaneity.py file
Fitting the sigma value to the excepted simultaneity factor for district heating grids is still possible, but has to be enabled with a new config setting 'simultaneity_fit_sigma'.
1 parent b79f32f commit 215c874

2 files changed

Lines changed: 153 additions & 114 deletions

File tree

lpagg/agg.py

Lines changed: 1 addition & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,13 @@
3333
The aggregator module is the core of the load profile aggregator project.
3434
3535
"""
36-
import copy
3736
import locale
3837
import numpy as np
3938
import pandas as pd # Pandas
4039
import os # Operaing System
4140
import matplotlib.pyplot as plt # Plotting library
4241
import yaml # Read YAML configuration files
4342
from scipy import optimize
44-
from scipy.optimize import minimize_scalar
4543
import logging
4644
import datetime
4745
# from memory_profiler import profile
@@ -217,34 +215,6 @@ def houses_sort(cfg):
217215

218216

219217
def aggregator_run(cfg):
220-
"""Run the aggregator to create the load profiles.
221-
222-
Args:
223-
cfg (dict): A dictionary containing the dicts 'settings' and 'houses'.
224-
225-
Returns:
226-
result_dict (dict): A dictionary containing the resulting DataFrames.
227-
``weather_data`` contains the input weather data combined with the
228-
aggregated load profiles. ``load_curve_houses`` contains all individual
229-
profiles of the houses. ``P_max_houses`` contains the peak power
230-
of each house.
231-
232-
"""
233-
fit_sigma = False
234-
df_houses = pd.DataFrame.from_dict(cfg['houses'], orient='index')
235-
if "sigma" in df_houses.columns:
236-
sigma_list = df_houses['sigma'].unique()
237-
if (sigma_list == 'fit').all():
238-
fit_sigma = True
239-
240-
if not fit_sigma:
241-
result_dict = _aggregator_run_unique_profils(cfg)
242-
else:
243-
result_dict = _aggregator_run_sigma_fit(cfg)
244-
return result_dict
245-
246-
247-
def _aggregator_run_unique_profils(cfg):
248218
"""Run the aggregator with the 'unique profiles workflow'.
249219
250220
Args:
@@ -276,86 +246,6 @@ def _aggregator_run_unique_profils(cfg):
276246
return result_dict
277247

278248

279-
def _aggregator_run_sigma_fit(cfg):
280-
"""Run the aggregator while fitting the simultaneity to literature data.
281-
282-
The complete aggregator process is performed in a loop to fit
283-
the resulting simultaneity factor to a literature value derived from
284-
the number of buildings connected to a district heating grid, by
285-
choosing the standard deviation sigma.
286-
287-
Args:
288-
cfg (dict): A dictionary containing the dicts 'settings' and 'houses'.
289-
290-
Returns:
291-
result_dict (dict): A dictionary containing the resulting DataFrames.
292-
``weather_data`` contains the input weather data combined with the
293-
aggregated load profiles. ``load_curve_houses`` contains all individual
294-
profiles of the houses. ``P_max_houses`` contains the peak power
295-
of each house.
296-
297-
"""
298-
cfg_backup = copy.deepcopy(cfg)
299-
cfg['settings']['print_houses_dat'] = False
300-
cfg['settings']['print_houses_xlsx'] = False
301-
cfg['settings']['calc_P_max'] = False
302-
cfg['settings']['print_P_max'] = False
303-
cfg['settings']['print_GLF_stats'] = False
304-
cfg['settings']['show_plot'] = False
305-
cfg['settings']['save_plot_filetypes'] = None
306-
307-
def black_box(sigma):
308-
sigma = abs(sigma)
309-
df_houses = pd.DataFrame.from_dict(cfg['houses'], orient='index')
310-
df_houses['sigma'] = sigma
311-
cfg['houses'] = df_houses.to_dict(orient='index')
312-
313-
_aggregator_run_unique_profils(cfg)
314-
315-
df_sf = pd.DataFrame.from_dict(
316-
cfg.get('simultaneity_factor_results', dict()), orient='index')
317-
if df_sf.empty:
318-
sf_lpagg = 1
319-
else:
320-
sf_lpagg = df_sf.loc['GLF', 'th']
321-
322-
sf_formula = simultaneity_factor(len(df_houses))
323-
f_sigma = abs(sf_lpagg - sf_formula)
324-
return f_sigma
325-
326-
opt_result = minimize_scalar(black_box, tol=1e-4,
327-
# options=dict(disp=3),
328-
)
329-
330-
sigma_result = opt_result.x
331-
332-
cfg = copy.deepcopy(cfg_backup)
333-
df_houses = pd.DataFrame.from_dict(cfg['houses'], orient='index')
334-
df_houses['sigma'] = sigma_result
335-
cfg['houses'] = df_houses.to_dict(orient='index')
336-
result_dict = _aggregator_run_unique_profils(cfg)
337-
338-
return result_dict
339-
340-
341-
def simultaneity_factor(n, decimals=5):
342-
"""Calculate simultaneity factor based on number of consumers n.
343-
344-
Winter, Walter; Haslauer, Thomas; Obernberger, Ingwald (2001)
345-
Untersuchungen der Gleichzeitigkeit in kleinen und
346-
mittleren Nahwärmenetzen. In: Euroheat & Power (9/10).
347-
"""
348-
n = np.asarray(n, dtype=float)
349-
a = 0.449677646267461
350-
b = 0.551234688
351-
c = 53.84382392
352-
d = 1.762743268
353-
f = a + b / (1 + (n / c) ** d)
354-
f = np.clip(f, a_min=0, a_max=1)
355-
f = np.round(f, decimals)
356-
return f
357-
358-
359249
def _aggregator_run(cfg):
360250
"""Run the aggregator to create the load profiles.
361251
@@ -508,6 +398,7 @@ def preprocess_unique_profiles(cfg):
508398
cfg['settings']['print_GLF_stats'] = False
509399
cfg['settings']['show_plot'] = False
510400
cfg['settings']['save_plot_filetypes'] = None
401+
cfg['settings']['simultaneity_fit_sigma'] = False
511402

512403
logger.info('Number of unique profiles: %s', len(df_unique))
513404

lpagg/simultaneity.py

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@
5959
"""
6060

6161
import os
62+
import copy
6263
import logging
6364
import pandas as pd
6465
import numpy as np
6566
import scipy.stats
67+
import scipy.optimize
6668
import matplotlib.pyplot as plt # Plotting library
6769
import matplotlib as mpl
6870

@@ -342,6 +344,149 @@ def copy_and_randomize(load_curve_houses, house_name, randoms_int, sigma,
342344
def copy_and_randomize_houses(load_curve_houses, houses_dict, cfg):
343345
"""Create copies of houses and randomize load where needed.
344346
347+
Args:
348+
load_curve_houses (DataFrame): Time series data to use.
349+
350+
houses_dict (dict): Dictionary with all the houses
351+
352+
cfg (dict): The configuration for the load profile aggregator
353+
354+
Returns:
355+
load_curve_houses (DataFrame): Manipulated time series data
356+
357+
"""
358+
if cfg['settings'].get('simultaneity_fit_sigma', False):
359+
load_curve_houses = _copy_and_randomize_houses_sigma_fit(
360+
load_curve_houses, houses_dict, cfg)
361+
else:
362+
load_curve_houses = _copy_and_randomize_houses(
363+
load_curve_houses, houses_dict, cfg)
364+
return load_curve_houses
365+
366+
367+
def _copy_and_randomize_houses_sigma_fit(load_curve_houses, houses_dict, cfg):
368+
"""Run the randomization while fitting the simultaneity to literature data.
369+
370+
The complete randomizer process is performed in a loop to fit
371+
the resulting simultaneity factor to a literature value derived from
372+
the number of buildings connected to a district heating grid, by
373+
choosing the standard deviation sigma.
374+
375+
Args:
376+
load_curve_houses (DataFrame): Time series data to use.
377+
378+
houses_dict (dict): Dictionary with all the houses
379+
380+
cfg (dict): The configuration for the load profile aggregator
381+
382+
Returns:
383+
load_curve_houses (DataFrame): Manipulated time series data
384+
385+
"""
386+
precision_sigma = 5
387+
plot_fitting = False
388+
389+
def black_box(sigma):
390+
_cfg = copy.deepcopy(cfg)
391+
_cfg['settings']['print_GLF_stats'] = False
392+
_cfg['settings']['show_plot'] = False
393+
_cfg['settings']['save_plot_filetypes'] = None
394+
395+
if isinstance(sigma, np.ndarray): # Compatibility with optimizer
396+
sigma = sigma[0]
397+
# Sigma must not be negative. Sigma=0 needs to be prevented, because
398+
# that would skip the output of simultaneity calculations
399+
sigma = max(abs(sigma), 0.0001)
400+
df_houses = pd.DataFrame.from_dict(houses_dict, orient='index')
401+
df_houses['sigma'] = round(sigma, precision_sigma)
402+
_houses_dict = df_houses.to_dict(orient='index')
403+
404+
_copy_and_randomize_houses(load_curve_houses, _houses_dict, _cfg)
405+
406+
df_sf = pd.DataFrame.from_dict(
407+
_cfg.get('simultaneity_factor_results', dict()), orient='index')
408+
if df_sf.empty:
409+
sf_lpagg = 1
410+
else:
411+
sf_lpagg = df_sf.loc['GLF', 'th']
412+
413+
sf_formula = simultaneity_factor(len(df_houses))
414+
f_sigma = abs(sf_lpagg - sf_formula)
415+
416+
if plot_fitting:
417+
try:
418+
df_result = pd.DataFrame({"x": [sigma], "f(x)": [f_sigma]})
419+
ax = df_test.plot()
420+
df_result.plot.scatter(ax=ax, x="x", y="f(x)")
421+
plt.show()
422+
except Exception:
423+
pass
424+
425+
# print("sf_formula", sf_formula, "sf_lpagg", sf_lpagg, "sigma", sigma)
426+
return f_sigma
427+
428+
if plot_fitting:
429+
x_test = np.arange(0, 20, 0.1)
430+
f_sigma_list = [black_box(sigma) for sigma in x_test]
431+
df_test = pd.Series(index=x_test, data=f_sigma_list)
432+
433+
opt_result = scipy.optimize.basinhopping(
434+
black_box,
435+
x0=[0],
436+
stepsize=2.0,
437+
niter=200,
438+
niter_success=10,
439+
# disp=True,
440+
minimizer_kwargs=dict(
441+
method="Powell",
442+
# tol=1e-7,
443+
options=dict(
444+
# maxfev=50,
445+
# xtol=1e-1,
446+
# ftol=1e-1,
447+
xtol=1e0,
448+
ftol=1e0,
449+
# disp=True,
450+
),
451+
),
452+
seed=np.random.default_rng(42),
453+
)
454+
455+
sigma_result = max(abs(opt_result.x), 0.0001)
456+
if isinstance(sigma_result, np.ndarray): # Compatibility with optimizer
457+
sigma_result = sigma_result[0]
458+
459+
# cfg = copy.deepcopy(cfg_backup)
460+
df_houses = pd.DataFrame.from_dict(cfg['houses'], orient='index')
461+
df_houses['sigma'] = round(sigma_result, precision_sigma)
462+
cfg['houses'].update(df_houses.to_dict(orient='index'))
463+
load_curve_houses = _copy_and_randomize_houses(
464+
load_curve_houses, houses_dict, cfg)
465+
466+
return load_curve_houses
467+
468+
469+
def simultaneity_factor(n, decimals=5):
470+
"""Calculate simultaneity factor based on number of consumers n.
471+
472+
Winter, Walter; Haslauer, Thomas; Obernberger, Ingwald (2001)
473+
Untersuchungen der Gleichzeitigkeit in kleinen und
474+
mittleren Nahwärmenetzen. In: Euroheat & Power (9/10).
475+
"""
476+
n = np.asarray(n, dtype=float)
477+
a = 0.449677646267461
478+
b = 0.551234688
479+
c = 53.84382392
480+
d = 1.762743268
481+
f = a + b / (1 + (n / c) ** d)
482+
f = np.clip(f, a_min=0, a_max=1)
483+
f = np.round(f, decimals)
484+
return f
485+
486+
487+
def _copy_and_randomize_houses(load_curve_houses, houses_dict, cfg):
488+
"""Create copies of houses and randomize load where needed.
489+
345490
Apply a normal distribution to the houses, if a standard deviation
346491
``sigma`` is given in the config. This function is an internal part
347492
of the load profile aggregator program.
@@ -371,7 +516,7 @@ def copy_and_randomize_houses(load_curve_houses, houses_dict, cfg):
371516
cfg (dict): The configuration for the load profile aggregator
372517
373518
Returns:
374-
load_curve_houses (DataFrame): Manipulatted time series data
519+
load_curve_houses (DataFrame): Manipulated time series data
375520
"""
376521
settings = cfg['settings']
377522
if settings.get('GLF_on', True) is False:
@@ -598,7 +743,10 @@ def plot_normal_histogram(house_name, randoms_int, save_folder=None,
598743
logger.debug('Interval shifts applied to ' + str(house_name) + ':')
599744
logger.debug(randoms_int)
600745
mu = np.mean(randoms_int)
601-
sigma = np.std(randoms_int, ddof=1)
746+
if len(randoms_int) > 1:
747+
sigma = np.std(randoms_int, ddof=1)
748+
else:
749+
sigma = np.nan
602750
text_mean_std = 'Mean = {:0.2f}, std = {:0.2f}'.format(mu, sigma)
603751
title_mu_std = r'$\mu={:0.3f},\ \sigma={:0.3f}$'.format(mu, sigma)
604752
logger.debug(text_mean_std)
@@ -615,7 +763,7 @@ def plot_normal_histogram(house_name, randoms_int, save_folder=None,
615763
ax.yaxis.grid(True) # Activate grid on horizontal axis
616764
n, bins, patches = plt.hist(randoms_int, bins, align='mid',
617765
rwidth=0.9, label=txt_label_data)
618-
if cfg['settings'].get('GLF_stats_include_normal', True):
766+
if cfg['settings'].get('GLF_stats_include_normal', True) and sigma > 0:
619767
# Create an ideal normal distribution for reference
620768
x_norm = np.linspace(min(bins), max(bins), 100)
621769
y_norm = scipy.stats.norm.pdf(x_norm, mu, sigma)
@@ -624,7 +772,7 @@ def plot_normal_histogram(house_name, randoms_int, save_folder=None,
624772
plt.title(str(len(randoms_int))+' '+str(house_name)+' ('+title_mu_std+')')
625773
plt.xlabel(txt_xlabel)
626774
plt.ylabel(txt_ylabel)
627-
if cfg['settings'].get('GLF_stats_include_normal', True):
775+
if cfg['settings'].get('GLF_stats_include_normal', True) and sigma > 0:
628776
plt.legend()
629777

630778
lpagg.misc.savefig_filetypes(

0 commit comments

Comments
 (0)