def test_get_forecast_start_end_time_tz(forecast_hr_begin, adjust_for_interval_label, issue, exp_start, exp_end): fx_start, fx_end = utils.get_forecast_start_end(forecast_hr_begin, issue, adjust_for_interval_label) assert fx_start == exp_start fx_end_exp = exp_end if adjust_for_interval_label: fx_end_exp -= pd.Timedelta('1n') assert fx_end == fx_end_exp
def test_get_forecast_start_end_time_same_as_issue_time_n_x_run( forecast_hr_begin, adjust_for_interval_label): # time is same as issue time of day + n * run_length issue_time = pd.Timestamp('20190422T1200') fx_start, fx_end = utils.get_forecast_start_end(forecast_hr_begin, issue_time, adjust_for_interval_label) assert fx_start == pd.Timestamp('20190422T1300') fx_end_exp = pd.Timestamp('20190422T1400') if adjust_for_interval_label: fx_end_exp -= pd.Timedelta('1n') assert fx_end == fx_end_exp
def test_get_forecast_start_end_time_same_as_issue_time( forecast_hr_begin, adjust_for_interval_label): # time is same as issue time of day issue_time = pd.Timestamp('20190422T0500') fx_start, fx_end = utils.get_forecast_start_end(forecast_hr_begin, issue_time, adjust_for_interval_label) assert fx_start == pd.Timestamp('20190422T0600') fx_end_exp = pd.Timestamp('20190422T0700') if adjust_for_interval_label: fx_end_exp -= pd.Timedelta('1n') assert fx_end == fx_end_exp
def test_get_forecast_start_end_time_instant(site_metadata, adjust_for_interval_label): # instant forecast = default_forecast(site_metadata, issue_time_of_day=dt.time(hour=5), lead_time_to_start=pd.Timedelta('1h'), interval_length=pd.Timedelta('5min'), run_length=pd.Timedelta('1h'), interval_label='instant') issue_time = pd.Timestamp('20190422T0500') fx_start, fx_end = utils.get_forecast_start_end(forecast, issue_time, adjust_for_interval_label) assert fx_start == pd.Timestamp('20190422T0600') fx_end_exp = pd.Timestamp('20190422T0700') if adjust_for_interval_label: fx_end_exp -= pd.Timedelta('1n') assert fx_end == fx_end_exp
def run_nwp(forecast, model, run_time, issue_time): """ Calculate benchmark irradiance and power forecasts for a Forecast or ProbabilisticForecast. Forecasts may be run operationally or retrospectively. For operational forecasts, *run_time* is typically set to now. For retrospective forecasts, *run_time* is the time by which the forecast should be run so that it could have been be delivered for the *issue_time*. Forecasts will only use data with timestamps before *run_time*. Parameters ---------- forecast : datamodel.Forecast or datamodel.ProbabilisticForecast The metadata of the desired forecast. model : function NWP model loading and processing function. See :py:mod:`solarforecastarbiter.reference_forecasts.models` for options. run_time : pd.Timestamp Run time of the forecast. issue_time : pd.Timestamp Issue time of the forecast run. Returns ------- ghi : pd.Series or pd.DataFrame dni : pd.Series or pd.DataFrame dhi : pd.Series or pd.DataFrame air_temperature : pd.Series or pd.DataFrame wind_speed : pd.Series or pd.DataFrame ac_power : pd.Series or pd.DataFrame Series are returned for deterministic forecasts, DataFrames are returned for probabilisic forecasts. Examples -------- The following code would return hourly average forecasts derived from the subhourly HRRR model. .. testsetup:: import datetime from solarforecastarbiter import datamodel from solarforecastarbiter.reference_forecasts import models from solarforecastarbiter.reference_forecasts.main import * >>> run_time = pd.Timestamp('20190515T0200Z') >>> issue_time = pd.Timestamp('20190515T0000Z') >>> modeling_parameters = datamodel.FixedTiltModelingParameters( ... surface_tilt=30, surface_azimuth=180, ... ac_capacity=10, dc_capacity=15, ... temperature_coefficient=-0.4, dc_loss_factor=0, ... ac_loss_factor=0) >>> power_plant = datamodel.SolarPowerPlant( ... name='Test plant', latitude=32.2, longitude=-110.9, ... elevation=715, timezone='America/Phoenix', ... modeling_parameters=modeling_parameters) >>> forecast = datamodel.Forecast( ... name='Test plant fx', ... site=power_plant, ... variable='ac_power', ... interval_label='ending', ... interval_value_type='mean', ... interval_length='1h', ... issue_time_of_day=datetime.time(hour=0), ... run_length=pd.Timedelta('24h'), ... lead_time_to_start=pd.Timedelta('0h')) >>> ghi, dni, dhi, temp_air, wind_speed, ac_power = run_nwp( ... forecast, models.hrrr_subhourly_to_hourly_mean, ... run_time, issue_time) """ fetch_metadata = fetch_nwp.model_map[models.get_nwp_model(model)] # absolute date and time for model run most recently available # as of run_time init_time = utils.get_init_time(run_time, fetch_metadata) # absolute start and end times. interval_label still controls # inclusive/exclusive start, end = utils.get_forecast_start_end(forecast, issue_time) site = forecast.site logger.info( 'Calculating forecast for model %s starting at %s from %s to %s', model, init_time, start, end) # model will account for interval_label *forecasts, resampler, solar_position_calculator = model( site.latitude, site.longitude, site.elevation, init_time, start, end, forecast.interval_label) if isinstance(site, datamodel.SolarPowerPlant): solar_position = solar_position_calculator() if isinstance(forecasts[0], pd.DataFrame): # must iterate over columns because pvmodel.irradiance_to_power # calls operations that do not properly broadcast Series along # a DataFrame time index. pvlib.irradiance.haydavies operation # (AI = dni_ens / dni_extra) is the known culprit, though there # may be more. ac_power = {} for col in forecasts[0].columns: member_fx = [fx.get(col) for fx in forecasts] member_ac_power = pvmodel.irradiance_to_power( site.modeling_parameters, solar_position['apparent_zenith'], solar_position['azimuth'], *member_fx) ac_power[col] = member_ac_power ac_power = pd.DataFrame(ac_power) else: ac_power = pvmodel.irradiance_to_power( site.modeling_parameters, solar_position['apparent_zenith'], solar_position['azimuth'], *forecasts) else: ac_power = None # resample data after power calculation resampled = list(map(resampler, (*forecasts, ac_power))) nwpoutput = namedtuple( 'NWPOutput', ['ghi', 'dni', 'dhi', 'air_temperature', 'wind_speed', 'ac_power']) return nwpoutput(*resampled)
def run_persistence(session, observation, forecast, run_time, issue_time, index=False): """ Run a persistence *forecast* for an *observation*. For intraday forecasts, the *index* argument controls if the forecast is constructed using persistence of the measured values (*index = False*) or persistence using clear sky index or AC power index. For day ahead forecasts, only persistence of measured values (*index = False*) is supported. Forecasts may be run operationally or retrospectively. For operational forecasts, *run_time* is typically set to now. For retrospective forecasts, *run_time* is the time by which the forecast should be run so that it could have been be delivered for the *issue_time*. Forecasts will only use data with timestamps before *run_time*. The persistence *window* is the time over which the persistence quantity (irradiance, power, clear sky index, or power index) is averaged. The persistence window is automatically determined from the *forecast* attributes: * Intraday persistence forecasts: *window = forecast.run_length*. No longer than 1 hour. * Day ahead forecasts (all but net load) and week ahead forecasts (net load only): *window = forecast.interval_length*. Users that would like more flexibility may use the lower-level functions in :py:mod:`solarforecastarbiter.reference_forecasts.persistence`. Parameters ---------- session : api.Session The session object to use to request data from the SolarForecastArbiter API. observation : datamodel.Observation The metadata of the observation to be used to create the forecast. forecast : datamodel.Forecast The metadata of the desired forecast. run_time : pd.Timestamp Run time of the forecast. issue_time : pd.Timestamp Issue time of the forecast run. index : bool, default False If False, use persistence of observed value. If True, use persistence of clear sky or AC power index. Returns ------- forecast : pd.Series Forecast conforms to the metadata specified by the *forecast* argument. Raises ------ ValueError If forecast and issue_time are incompatible. ValueError If persistence window < observation.interval_length. ValueError If forecast.run_length = 1 day and forecast period is not midnight to midnight. ValueError If forecast.run_length = 1 day and index=True. ValueError If instantaneous forecast and instantaneous observation interval lengths do not match. ValueError If average observations are used to make instantaneous forecast. Notes ----- For non-intraday net load forecasts, this function will use a weekahead persistence due to the fact that net load exhibits stronger correlation week-to-week than day-to-day. For example, the net load on a Monday tends to look more similar to the previous Monday that it does to the previous day (Sunday). """ utils.check_persistence_compatibility(observation, forecast, index) forecast_start, forecast_end = utils.get_forecast_start_end( forecast, issue_time, False) intraday = utils._is_intraday(forecast) if not intraday: # raise ValueError if not intraday and not midnight to midnight utils._check_midnight_to_midnight(forecast_start, forecast_end) data_start, data_end = utils.get_data_start_end(observation, forecast, run_time) def load_data(observation, data_start, data_end): df = session.get_observation_values(observation.observation_id, data_start, data_end, observation.interval_label) df = df.tz_convert(observation.site.timezone) return df['value'] if intraday and index: fx = persistence.persistence_scalar_index( observation, data_start, data_end, forecast_start, forecast_end, forecast.interval_length, forecast.interval_label, load_data) elif intraday and not index: fx = persistence.persistence_scalar(observation, data_start, data_end, forecast_start, forecast_end, forecast.interval_length, forecast.interval_label, load_data) elif not intraday and not index: fx = persistence.persistence_interval(observation, data_start, data_end, forecast_start, forecast.interval_length, forecast.interval_label, load_data) else: # pragma: no cover raise ValueError( 'index=True not supported for forecasts with run_length >= 1day') return fx
def test_get_forecast_start_end_time_invalid(forecast_hr_begin): # time is invalid issue_time = pd.Timestamp('20190423T0505') with pytest.raises(ValueError): fx_start, fx_end = utils.get_forecast_start_end( forecast_hr_begin, issue_time)
def test_get_forecast_start_end_time_before_issue_time(forecast_hr_begin): # time is before issue time of day but otherwise valid issue_time = pd.Timestamp('20190422T0400') with pytest.raises(ValueError): fx_start, fx_end = utils.get_forecast_start_end( forecast_hr_begin, issue_time)
def run_persistence(session, observation, forecast, run_time, issue_time, index=False, load_data=None): """ Run a persistence *forecast* for an *observation*. For intraday forecasts, the *index* argument controls if the forecast is constructed using persistence of the measured values (*index = False*) or persistence using clear sky index or AC power index. For day ahead forecasts, only persistence of measured values (*index = False*) is supported. Forecasts may be run operationally or retrospectively. For operational forecasts, *run_time* is typically set to now. For retrospective forecasts, *run_time* is the time by which the forecast should be run so that it could have been be delivered for the *issue_time*. Forecasts will only use data with timestamps before *run_time*. The persistence *window* is the time over which the persistence quantity (irradiance, power, clear sky index, or power index) is averaged. The persistence window is automatically determined from the *forecast* attributes: - Intraday persistence forecasts: + ``window = forecast.run_length``. No longer than 1 hour. - Day ahead forecasts (all but net load) and week ahead forecasts (net load only): + ``window = forecast.interval_length``. Users that would like more flexibility may use the lower-level functions in :py:mod:`solarforecastarbiter.reference_forecasts.persistence`. Parameters ---------- session : api.Session The session object to use to request data from the SolarForecastArbiter API. observation : datamodel.Observation The metadata of the observation to be used to create the forecast. forecast : datamodel.Forecast The metadata of the desired forecast. run_time : pd.Timestamp Run time of the forecast. issue_time : pd.Timestamp Issue time of the forecast run. index : bool, default False If False, use persistence of observed value. If True, use persistence of clear sky or AC power index. load_data : function Function to load the observation data 'value' series given (observation, data_start, data_end) arguments. Typically, calls `session.get_observation_values` and selects the 'value' column. May also have data preloaded to then slice from data_start to data_end. Returns ------- forecast : pd.Series Forecast conforms to the metadata specified by the *forecast* argument. Raises ------ ValueError If forecast and issue_time are incompatible. ValueError If data is required from after run_time. ValueError If persistence window < observation.interval_length. ValueError If forecast.run_length => 1 day and index=True. ValueError If instantaneous forecast and instantaneous observation interval lengths do not match. ValueError If average observations are used to make instantaneous forecast. Notes ----- For non-intraday net load forecasts, this function will use a weekahead persistence due to the fact that net load exhibits stronger correlation week-to-week than day-to-day. For example, the net load on a Monday tends to look more similar to the previous Monday that it does to the previous day (Sunday). """ utils.check_persistence_compatibility(observation, forecast, index) forecast_start, forecast_end = utils.get_forecast_start_end( forecast, issue_time, False) intraday = utils._is_intraday(forecast) if load_data is None: load_data = _default_load_data(session) data_start, data_end = utils.get_data_start_end(observation, forecast, run_time, issue_time) if data_end > run_time: raise ValueError( 'Persistence forecast requires data from after run_time') if isinstance(forecast, datamodel.ProbabilisticForecast): cvs = [f.constant_value for f in forecast.constant_values] fx = persistence.persistence_probabilistic( observation, data_start, data_end, forecast_start, forecast_end, forecast.interval_length, forecast.interval_label, load_data, forecast.axis, cvs) elif intraday and index: fx = persistence.persistence_scalar_index( observation, data_start, data_end, forecast_start, forecast_end, forecast.interval_length, forecast.interval_label, load_data) elif intraday and not index: fx = persistence.persistence_scalar(observation, data_start, data_end, forecast_start, forecast_end, forecast.interval_length, forecast.interval_label, load_data) elif not intraday and not index: fx = persistence.persistence_interval(observation, data_start, data_end, forecast_start, forecast.interval_length, forecast.interval_label, load_data) else: # pragma: no cover raise ValueError( 'index=True not supported for forecasts with run_length >= 1day') return fx