-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue135 forecast uncertainty #482
Changes from all commits
eab2f75
79cbeb2
0dc3f89
f4093d1
407bc79
cac66b6
e66ea46
c74022e
9d33819
816358c
edeaf1c
0267d23
b5db17d
2597023
b6fb4a7
bc6f233
5eebe2b
32f727f
a433f7c
c29d489
a7560fb
d81a697
5c200c8
7fb0965
3ffc633
b1f38f1
8c29dbf
f503671
a4bc294
7e001b4
e229f4d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,7 @@ testcase2/doc/build/ | |
parser/*.fmu | ||
parser/*.mo | ||
parser/*.json | ||
|
||
xx.ipynb | ||
connect_boptest.py | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a connect_boptest.py and don't think this should be written in the .gitignore. |
||
\! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be in the .gitignore. |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
''' | ||
Created on Sept 5, 2022 | ||
|
||
@author: Laura Zabala | ||
|
||
''' | ||
|
||
import numpy as np | ||
|
||
|
||
|
||
def mean_filter(data, window_size=3): | ||
""" | ||
Apply mean filter to a 1D list of data. | ||
|
||
Parameters: | ||
- data: List of numbers to be filtered. | ||
- window_size: Size of the filtering window. Must be an odd number. | ||
|
||
Returns: | ||
- Filtered data. | ||
""" | ||
if window_size % 2 == 0: | ||
raise ValueError("Window size must be an odd number.") | ||
|
||
half_window = window_size // 2 | ||
filtered_data = data.copy() | ||
|
||
for i in range(half_window, len(data) - half_window): | ||
if data[i] == 0: | ||
continue | ||
|
||
window_data = data[i - half_window : i + half_window + 1] | ||
filtered_data[i] = sum(window_data) / len(window_data) | ||
|
||
return filtered_data | ||
def predict_temperature_error_AR1(hp, F0, K0, F, K, mu): | ||
''' | ||
Generates an error for the temperature forecast with an AR model with normal distribution in the hp points of the predictions horizon. | ||
|
||
Parameters | ||
---------- | ||
hp : int | ||
Number of points in the prediction horizon. | ||
F0 : float | ||
Mean of the initial error model. | ||
K0 : float | ||
Standard deviation of the initial error model. | ||
F : float | ||
Autocorrelation factor of the AR error model, value should be between 0 and 1. | ||
K : float | ||
Standard deviation of the AR error model. | ||
mu : float | ||
Mean value of the distribution function integrated in the AR error model. | ||
|
||
|
||
Returns | ||
------- | ||
error : 1D array | ||
Array containing the error values in the hp points. | ||
''' | ||
|
||
|
||
error = np.zeros(hp) | ||
error[0] = np.random.normal(F0, K0) | ||
for i_c in range(hp - 1): | ||
error[i_c + 1] = np.random.normal( | ||
error[i_c] * F + mu, K | ||
) | ||
return error | ||
|
||
|
||
|
||
def predict_solar_error_AR1(hp, ag0, bg0, phi, ag, bg): | ||
'''Generates an error for the solar forecast based on the specified parameters using an AR model with Laplace distribution in the hp points of the predictions horizon. | ||
|
||
Parameters | ||
---------- | ||
hp : int | ||
Number of points in the prediction horizon. | ||
ag0, bg0, phi, ag, bg : float | ||
Parameters for the AR1 model. | ||
|
||
Returns | ||
------- | ||
error : 1D numpy array | ||
Contains the error values in the hp points. | ||
''' | ||
|
||
error = np.zeros(hp) | ||
error[0] = np.random.laplace(ag0, bg0) | ||
for i_c in range(1, hp): | ||
error[i_c] = np.random.laplace(error[i_c - 1] * phi + ag, bg) | ||
|
||
return error |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{ | ||
"temperature": { | ||
"low": { | ||
"F0": 0, | ||
"K0": 0.6, | ||
"F": 0.92, | ||
"K": 0.4, | ||
"mu": 0 | ||
}, | ||
"medium": { | ||
"F0": 0.15, | ||
"K0": 1.2, | ||
"F": 0.93, | ||
"K": 0.6, | ||
"mu": 0 | ||
}, | ||
"high": { | ||
"F0": -0.58, | ||
"K0": 1.5, | ||
"F": 0.95, | ||
"K": 0.7, | ||
"mu": -0.015 | ||
} | ||
}, | ||
"solar": { | ||
"low": { | ||
"ag0": 4.44, | ||
"bg0": 57.42, | ||
"phi": 0.62, | ||
"ag": 1.86, | ||
"bg": 45.64 | ||
}, | ||
"medium": { | ||
"ag0": 15.02, | ||
"bg0": 122.6, | ||
"phi": 0.63, | ||
"ag": 4.44, | ||
"bg": 91.97 | ||
}, | ||
"high": { | ||
"ag0": 32.09, | ||
"bg0": 119.94, | ||
"phi": 0.67, | ||
"ag": 10.63, | ||
"bg": 87.44 | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,16 @@ | ||
''' | ||
Created on Apr 25, 2019 | ||
|
||
@author: Javier Arroyo | ||
@author: Javier Arroyo, Laura Zabala and Wanfu Zheng | ||
|
||
This module contains the Forecaster class with methods to obtain | ||
forecast data for the test case. It relies on the data_manager object | ||
of the test case to provide deterministic forecast. | ||
|
||
''' | ||
from .error_emulator import predict_temperature_error_AR1, predict_solar_error_AR1, mean_filter | ||
import numpy as np | ||
|
||
|
||
class Forecaster(object): | ||
'''This class retrieves test case data forecast for its use in | ||
|
@@ -30,45 +33,146 @@ def __init__(self, testcase): | |
# Point to the test case object | ||
self.case = testcase | ||
|
||
def get_forecast(self,point_names, horizon=24*3600, interval=3600, | ||
category=None, plot=False): | ||
'''Returns forecast of the test case data | ||
def get_forecast(self, point_names, horizon=24 * 3600, interval=3600, | ||
wea_tem_dry_bul=None, wea_sol_glo_hor=None, seed=None): | ||
''' | ||
Retrieves forecast data for specified points over a given horizon and interval. | ||
|
||
Parameters | ||
---------- | ||
point_names : list of str | ||
List of forecast point names for which to get data. | ||
horizon : int, default is 86400 (one day) | ||
Length of the requested forecast in seconds. If None, | ||
the test case horizon will be used instead. | ||
interval : int, default is 3600 (one hour) | ||
resampling time interval in seconds. If None, | ||
the test case interval will be used instead. | ||
category : string, default is None | ||
Type of data to retrieve from the test case. | ||
If None it will return all available test case | ||
data without filtering it by any category. | ||
Possible options are 'weather', 'prices', | ||
'emissions', 'occupancy', internalGains, 'setpoints' | ||
plot : boolean, default is False | ||
True if desired to plot the forecast | ||
List of data point names for which the forecast is to be retrieved. | ||
horizon : int, optional | ||
Forecast horizon in seconds (default is 86400 seconds, i.e., one day). | ||
interval : int, optional | ||
Time interval between forecast points in seconds (default is 3600 seconds, i.e., one hour). | ||
wea_tem_dry_bul : dict, optional | ||
Parameters for the AR1 model to simulate forecast error in dry bulb temperature: | ||
- F0, K0, F, K, mu : parameters used in the AR1 model. | ||
If None, defaults to a dictionary with all parameters set to zero, simulating no forecast error. | ||
wea_sol_glo_hor : dict, optional | ||
Parameters for the AR1 model to simulate forecast error in global horizontal solar irradiation: | ||
- ag0, bg0, phi, ag, bg : parameters used in the AR1 model. | ||
If None, defaults to a dictionary with all parameters set to zero, simulating no forecast error. | ||
seed : int, optional | ||
Seed for the random number generator to ensure reproducibility of the stochastic forecast error. | ||
|
||
Returns | ||
------- | ||
forecast : dict | ||
Dictionary with the requested forecast data | ||
{<variable_name>:<variable_forecast_trajectory>} | ||
where <variable_name> is a string with the variable | ||
key and <variable_forecast_trajectory> is a list with | ||
the forecasted values. 'time' is included as a variable | ||
A dictionary containing the forecast data for the requested points with applied error models. | ||
======= | ||
weather_temperature_dry_bulb=None, weather_solar_global_horizontal=None, seed=None, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest shorter variable names, using three letters, such as: Also missing documentation of these parameters, which should be included as docstrings as in other functions, and was done previously for this one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your suggestions regarding the variable names, the documentation and the issue of '.gitignore' file. I have updated the variable names to shorter versions as you suggested, updated the docstrings and corrected the '.gitignore'. I've created a pull request to merge these modifications into the issue135_forecastUncertainty branch of the forked repository at laura-zabala/project1-boptest. |
||
category=None, plot=False): | ||
|
||
''' | ||
if weather_temperature_dry_bulb is None: | ||
weather_temperature_dry_bulb = { | ||
"F0": 0, "K0": 0, "F": 0, "K": 0, "mu": 0 | ||
} | ||
|
||
if weather_solar_global_horizontal is None: | ||
weather_solar_global_horizontal = { | ||
"ag0": 0, "bg0": 0, "phi": 0, "ag": 0, "bg": 0 | ||
} | ||
# Get the forecast | ||
forecast = self.case.data_manager.get_data(variables=point_names, | ||
horizon=horizon, | ||
interval=interval, | ||
category=category, | ||
plot=plot) | ||
interval=interval) | ||
|
||
if 'TDryBul' in point_names and any(wea_tem_dry_bul.values()): | ||
if seed is not None: | ||
np.random.seed(seed) | ||
# error in the forecast | ||
error_forecast_temp = predict_temperature_error_AR1( | ||
hp=int(horizon / interval + 1), | ||
F0=wea_tem_dry_bul["F0"], | ||
K0=wea_tem_dry_bul["K0"], | ||
F=wea_tem_dry_bul["F"], | ||
K=wea_tem_dry_bul["K"], | ||
mu=wea_tem_dry_bul["mu"] | ||
) | ||
|
||
# forecast error just added to dry bulb temperature | ||
forecast['TDryBul'] = forecast['TDryBul'] - error_forecast_temp | ||
forecast['TDryBul'] = forecast['TDryBul'].tolist() | ||
if 'HGloHor' in point_names and any(wea_sol_glo_hor.values()): | ||
|
||
original_HGloHor = np.array(forecast['HGloHor']).copy() | ||
lower_bound = 0.2 * original_HGloHor | ||
upper_bound = 2 * original_HGloHor | ||
indices = np.where(original_HGloHor > 50)[0] | ||
|
||
|
||
for i in range(200): | ||
if seed is not None: | ||
np.random.seed(seed+i*i) | ||
error_forecast_solar = predict_solar_error_AR1( | ||
int(horizon / interval + 1), | ||
wea_sol_glo_hor["ag0"], | ||
wea_sol_glo_hor["bg0"], | ||
wea_sol_glo_hor["phi"], | ||
wea_sol_glo_hor["ag"], | ||
wea_sol_glo_hor["bg"] | ||
) | ||
|
||
forecast['HGloHor'] = original_HGloHor - error_forecast_solar | ||
|
||
# Check if any point in forecast['HGloHor'] is out of the specified range | ||
condition = np.any((forecast['HGloHor'][indices] > 2 * original_HGloHor[indices]) | | ||
(forecast['HGloHor'][indices] < 0.2 * original_HGloHor[indices])) | ||
# forecast['HGloHor']=gaussian_filter_ignoring_nans(forecast['HGloHor']) | ||
forecast['HGloHor'] = mean_filter(forecast['HGloHor']) | ||
forecast['HGloHor'] = np.clip(forecast['HGloHor'], lower_bound, upper_bound) | ||
forecast['HGloHor'] = forecast['HGloHor'].tolist() | ||
if not condition: | ||
break | ||
|
||
if 'TDryBul' in point_names and any(weather_temperature_dry_bulb.values()): | ||
if seed is not None: | ||
np.random.seed(seed) | ||
# error in the forecast | ||
error_forecast_temp = predict_temperature_error_AR1( | ||
hp=int(horizon / interval + 1), | ||
F0=weather_temperature_dry_bulb["F0"], | ||
K0=weather_temperature_dry_bulb["K0"], | ||
F=weather_temperature_dry_bulb["F"], | ||
K=weather_temperature_dry_bulb["K"], | ||
mu=weather_temperature_dry_bulb["mu"] | ||
) | ||
|
||
# forecast error just added to dry bulb temperature | ||
forecast['TDryBul'] = forecast['TDryBul'] - error_forecast_temp | ||
forecast['TDryBul'] = forecast['TDryBul'].tolist() | ||
if 'HGloHor' in point_names and any(weather_solar_global_horizontal.values()): | ||
|
||
original_HGloHor = np.array(forecast['HGloHor']).copy() | ||
lower_bound = 0.2 * original_HGloHor | ||
upper_bound = 2 * original_HGloHor | ||
indices = np.where(original_HGloHor > 50)[0] | ||
|
||
|
||
for i in range(200): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it ever the case 200 is not enough? What happens if condition not met after 200? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if seed is not None: | ||
np.random.seed(seed+i*i) | ||
error_forecast_solar = predict_solar_error_AR1( | ||
int(horizon / interval + 1), | ||
weather_solar_global_horizontal["ag0"], | ||
weather_solar_global_horizontal["bg0"], | ||
weather_solar_global_horizontal["phi"], | ||
weather_solar_global_horizontal["ag"], | ||
weather_solar_global_horizontal["bg"] | ||
) | ||
|
||
forecast['HGloHor'] = original_HGloHor - error_forecast_solar | ||
|
||
# Check if any point in forecast['HGloHor'] is out of the specified range | ||
condition = np.any((forecast['HGloHor'][indices] > 2 * original_HGloHor[indices]) | | ||
(forecast['HGloHor'][indices] < 0.2 * original_HGloHor[indices])) | ||
# forecast['HGloHor']=gaussian_filter_ignoring_nans(forecast['HGloHor']) | ||
forecast['HGloHor'] = mean_filter(forecast['HGloHor']) | ||
forecast['HGloHor'] = np.clip(forecast['HGloHor'], lower_bound, upper_bound) | ||
forecast['HGloHor'] = forecast['HGloHor'].tolist() | ||
if not condition: | ||
break | ||
|
||
return forecast |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why this should be in the .gitignore.