Skip to content
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

Update forecaster #189

Merged
merged 3 commits into from
Dec 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 38 additions & 18 deletions src/pymgrid/forecast/forecaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from abc import abstractmethod


def get_forecaster(forecaster, forecast_horizon, time_series=None, increase_uncertainty=False):
def get_forecaster(forecaster, forecast_horizon, observation_space, time_series=None, increase_uncertainty=False):
"""
Get the forecasting function for the time series module.

Expand All @@ -25,7 +25,12 @@ def get_forecaster(forecaster, forecast_horizon, time_series=None, increase_unce

If None, no forecast.

forecast_horizon: int. Number of steps in the future to forecast. If forecaster is None, ignored and 0 is returned.
forecast_horizon: int
Number of steps in the future to forecast. If forecaster is None, ignored and 0 is returned.

observation_space: :class:`.ModuleSpace`
Observation space; used to determine values to pad missing forecasts when we are forecasting past the
end of the time series.

time_series: ndarray[float] or None, default None.
The underlying time series, used to validate UserDefinedForecaster.
Expand All @@ -36,26 +41,34 @@ def get_forecaster(forecaster, forecast_horizon, time_series=None, increase_unce

Returns
-------
forecast, callable[float, float, int].
forecaster : callable[float, float, int]
The forecasting function.

"""

if forecaster is None:
return NoForecaster(), 0
return NoForecaster(observation_space)
elif isinstance(forecaster, (UserDefinedForecaster, OracleForecaster, GaussianNoiseForecaster)):
return forecaster, forecast_horizon
return forecaster
elif callable(forecaster):
return UserDefinedForecaster(forecaster, time_series), forecast_horizon
return UserDefinedForecaster(forecaster, observation_space, time_series)
elif forecaster == "oracle":
return OracleForecaster(), forecast_horizon
return OracleForecaster(observation_space)
elif is_number(forecaster):
return GaussianNoiseForecaster(forecaster, increase_uncertainty=increase_uncertainty), forecast_horizon
return GaussianNoiseForecaster(
forecaster,
observation_space,
increase_uncertainty=increase_uncertainty
)
else:
raise ValueError(f"Unable to parse forecaster of type {type(forecaster)}")


class Forecaster:
def __init__(self, observation_space):
self._observation_space = observation_space
self._fill_arr = (self._observation_space.unnormalized.high - self._observation_space.unnormalized.low) / 2

@abstractmethod
def _forecast(self, val_c, val_c_n, n):
pass
Expand All @@ -64,14 +77,16 @@ def _pad(self, forecast, n):
if forecast.shape[0] == n:
return forecast
else:
pad_amount = n-forecast.shape[0]
return np.pad(forecast, ((0, pad_amount), (0, 0)), constant_values=0)
pad_amount = n - forecast.shape[0]
pad = self._fill_arr.reshape((-1, forecast.shape[1]))[-pad_amount:]
return np.concatenate((forecast, pad))

def __eq__(self, other):
if type(self) != type(other):
return NotImplemented

return self.__dict__ == other.__dict__
return (self._fill_arr == other._fill_arr).all() and \
all(v == other.__dict__[k] for k, v in self.__dict__.items() if k != '_fill_arr')

def __call__(self, val_c, val_c_n, n):
if len(val_c_n.shape) == 1:
Expand All @@ -90,7 +105,7 @@ def __repr__(self):


class UserDefinedForecaster(Forecaster):
def __init__(self, forecaster_function, time_series):
def __init__(self, forecaster_function, observation_space, time_series):
self.is_vectorized_forecaster, self.cast_to_arr = \
_validate_callable_forecaster(forecaster_function, time_series)

Expand All @@ -99,6 +114,8 @@ def __init__(self, forecaster_function, time_series):

self._forecaster = forecaster_function

super().__init__(observation_space)

def _cast_to_arr(self, forecast, val_c_n):
if self.cast_to_arr:
return np.array(forecast.reshape(val_c_n.shape))
Expand All @@ -115,15 +132,17 @@ def _forecast(self, val_c, val_c_n, n):


class GaussianNoiseForecaster(Forecaster):
def __init__(self, noise_std, increase_uncertainty=False):
def __init__(self, noise_std, observation_space, increase_uncertainty=False):
self.input_noise_std = noise_std
self.increase_uncertainty = increase_uncertainty
self._noise_size = None
self._noise_std = None

super().__init__(observation_space)

def _get_noise_std(self):
if self.increase_uncertainty:
return self.input_noise_std*(1+np.log(1+np.arange(self._noise_size)))
return self.input_noise_std * (1 + np.log(1 + np.arange(self._noise_size)))
else:
return self.input_noise_std

Expand All @@ -142,7 +161,7 @@ def noise_std(self):

def _forecast(self, val_c, val_c_n, n):
forecast = val_c_n + self._get_noise(len(val_c_n)).reshape(val_c_n.shape)
forecast[(forecast*val_c_n) < 0] = 0
forecast[(forecast * val_c_n) < 0] = 0
return forecast

def __repr__(self):
Expand Down Expand Up @@ -176,8 +195,8 @@ def _validate_vectorized_forecaster(forecaster, val_c, vector_true_forecast, n):
vectorized_forecast = forecaster(val_c, vector_true_forecast, n)
except Exception as e:
raise NotImplementedError("Unable to call forecaster with vector inputs. "
f"\nFunc call forecaster(val_c={val_c}, val_c_n={vector_true_forecast}, n={n})"
f"\nraised {type(e).__name__}: {e}") from e
f"\nFunc call forecaster(val_c={val_c}, val_c_n={vector_true_forecast}, n={n})"
f"\nraised {type(e).__name__}: {e}") from e
else:
# vectorized function call succeeded
if not hasattr(vectorized_forecast, 'size'):
Expand Down Expand Up @@ -217,7 +236,7 @@ def _validate_scalar_forecaster(forecaster, val_c, scalar_true_forecast, n):
scalar_forecast_item = scalar_forecast.item()
except (ValueError, AttributeError):
raise ValueError("Unable to validate forecaster. Forecaster must return scalar output with scalar "
f"input but returned {scalar_forecast}")
f"input but returned {scalar_forecast}")

_validate_forecasted_value(scalar_forecast_item, scalar_true_forecast, val_c, n)

Expand All @@ -244,4 +263,5 @@ def vectorized(val_c, val_c_n, n):
except IndexError:
shape = (-1, 1)
return vectorized_output.reshape(shape)

return vectorized
14 changes: 10 additions & 4 deletions src/pymgrid/modules/base/timeseries/base_timeseries_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ def __init__(self,
provided_energy_name='provided_energy',
absorbed_energy_name='absorbed_energy',
normalize_pos=...):

self._time_series = self._set_time_series(time_series)
self._min_obs, self._max_obs, self._min_act, self._max_act = self._get_bounds()

self._forecast_param = forecaster
self.forecaster, self._forecast_horizon = get_forecaster(forecaster,
forecast_horizon,
self.time_series,
increase_uncertainty=forecaster_increase_uncertainty)
self._forecast_horizon = forecast_horizon * (forecaster is not None)
self.forecaster = get_forecaster(
forecaster,
forecast_horizon,
self._get_observation_spaces(),
self.time_series,
increase_uncertainty=forecaster_increase_uncertainty
)

self._state_dict_keys = {"current": [f"{component}_current" for component in self.state_components],
"forecast": [
Expand Down
7 changes: 5 additions & 2 deletions src/pymgrid/utils/space.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
class ModuleSpace(Space):
def __init__(self, unnormalized_low, unnormalized_high, shape=None, dtype=np.float64, seed=None):

self._unnormalized = Box(low=unnormalized_low.astype(np.float64),
high=unnormalized_high.astype(np.float64),
low = np.float64(unnormalized_low) if np.isscalar(unnormalized_low) else unnormalized_low.astype(np.float64)
high = np.float64(unnormalized_high) if np.isscalar(unnormalized_high) else unnormalized_high.astype(np.float64)

self._unnormalized = Box(low=low,
high=high,
shape=shape,
dtype=dtype)

Expand Down
Loading