def test_irradiance_to_power(modeling_parameters_system_type, apparent_zenith, azimuth, ghi, dni, dhi, temp_air, wind_speed): modeling_parameters, system_type = modeling_parameters_system_type out = pvmodel.irradiance_to_power(modeling_parameters, apparent_zenith, azimuth, ghi, dni, dhi, temp_air=temp_air, wind_speed=wind_speed) index = apparent_zenith.index expected_fixed = pd.Series([0., 0.003], index=index) expected_tracking = pd.Series([0., 0.00293178], index=index) fixed_or_tracking(system_type, expected_fixed, expected_tracking, out)
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(site, model, init_time, start, end): """ Calculate benchmark irradiance and power forecasts for a site. The meaning of the timestamps (instantaneous or interval average) is determined by the model processing function. It's currently the user's job to determine time parameters that correspond to a particular Forecast Evaluation Time Series. Parameters ---------- site : datamodel.Site model : function NWP model loading and processing function. See :py:mod:`solarforecastarbiter.reference_forecasts.models` for options. init_time : pd.Timestamp NWP model initialization time. start : pd.Timestamp Start of the forecast. end : pd.Timestamp End of the forecast. Returns ------- ghi : pd.Series dni : pd.Series dhi : pd.Series temp_air : None or pd.Series wind_speed : None or pd.Series ac_power : None or pd.Series Examples -------- The following code would return hourly average forecasts derived from the subhourly HRRR model. >>> from solarforecastarbiter import datamodel >>> from solarforecastarbiter.reference_forecasts import models >>> init_time = pd.Timestamp('20190328T1200Z') >>> start = pd.Timestamp('20190328T1300Z') # typical available time >>> end = pd.Timestamp('20190329T1300Z') # 24 hour forecast >>> modeling_parameters = datamodel.FixedTiltModelingParameters( ... ac_capacity=10, dc_capacity=15, ... temperature_coefficient=-0.004, 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) >>> ghi, dni, dhi, temp_air, wind_speed, ac_power = run( ... power_plant, models.hrrr_subhourly_to_hourly_mean, ... init_time, start, end) """ *forecast, resampler, solar_position_calculator = model( site.latitude, site.longitude, site.elevation, init_time, start, end) if isinstance(site, datamodel.SolarPowerPlant): solar_position = solar_position_calculator() ac_power = pvmodel.irradiance_to_power( site.modeling_parameters, solar_position['apparent_zenith'], solar_position['azimuth'], *forecast) else: ac_power = None # resample data after power calculation resampled = list(map(resampler, (*forecast, ac_power))) return resampled
def persistence_scalar_index(observation, data_start, data_end, forecast_start, forecast_end, interval_length, interval_label, load_data): r""" Calculate a persistence forecast using the mean value of the *observation* clear sky index or AC power index from *data_start* to *data_end*. In the example below, we use GHI to be concrete but the concept also applies to AC power. The persistence forecast is: .. math:: GHI_{t_f} = \overline{ \frac{ GHI_{t_{start}} }{ GHI_{{clear}_{t_{start}}} } \ldots \frac{ GHI_{t_{end}} }{ GHI_{{clear}_{t_{end}}} } } where :math:`t_f` is a forecast time, and the overline represents the average of all observations or clear sky values that occur between :math:`t_{start}` = *data_start* and :math:`t_{end}` = *data_end*. All :math:`GHI_{t}/GHI_{{clear}_t}` ratios are restricted to the range [0, 2] before the average is computed. Parameters ---------- observation : datamodel.Observation data_start : pd.Timestamp Observation data start. Forecast is inclusive of this instant if observation.interval_label is *beginning* or *instant*. data_end : pd.Timestamp Observation data end. Forecast is inclusive of this instant if observation.interval_label is *ending* or *instant*. forecast_start : pd.Timestamp Forecast start. Forecast is inclusive of this instant if interval_label is *beginning* or *instant*. forecast_end : pd.Timestamp Forecast end. Forecast is inclusive of this instant if interval_label is *ending* or *instant*. interval_length : pd.Timedelta Forecast interval length interval_label : str instant, beginning, or ending load_data : function A function that loads the observation data. Must have the signature load_data(observation, data_start, data_end) and properly account for observation interval label. Returns ------- forecast : pd.Series The persistence forecast. The forecast interval label is the same as the observation interval label. """ # ensure that we're using times rounded to multiple of interval_length _check_intervals_times(observation.interval_label, data_start, data_end, forecast_start, forecast_end, observation.interval_length) # get observation data for specified range obs = load_data(observation, data_start, data_end) # partial-up the metadata for solar position and # clearsky calculation clarity and consistency site = observation.site calc_solpos = partial(pvmodel.calculate_solar_position, site.latitude, site.longitude, site.elevation) calc_cs = partial(pvmodel.calculate_clearsky, site.latitude, site.longitude, site.elevation) # Calculate solar position and clearsky for obs time range. # if data is instantaneous, calculate at the obs time. # else (if data is interval average), calculate at 1 minute resolution to # reduce errors from changing solar position during persistence data range. # Later, modeled clear sky or ac power will be averaged over the data range closed = datamodel.CLOSED_MAPPING[observation.interval_label] if closed is None: freq = observation.interval_length else: freq = pd.Timedelta('1min') obs_range = pd.date_range(start=data_start, end=data_end, freq=freq, closed=closed) solar_position_obs = calc_solpos(obs_range) clearsky_obs = calc_cs(solar_position_obs['apparent_zenith']) # Calculate solar position and clearsky for the forecast times. # Use 5 minute or better frequency to minimize solar position errors. # Later, modeled clear sky or ac power will be resampled to interval_length closed_fx = datamodel.CLOSED_MAPPING[interval_label] fx_range = pd.date_range(start=forecast_start, end=forecast_end, freq=freq, closed=closed_fx) solar_position_fx = calc_solpos(fx_range) clearsky_fx = calc_cs(solar_position_fx['apparent_zenith']) # Consider putting the code within each if/else block below into its own # function with standard outputs clear_ref and clear_fx. But with only two # cases for now, it might be more clear to leave inline. if ( isinstance(site, datamodel.SolarPowerPlant) and observation.variable == 'ac_power' ): # No temperature input is only OK so long as temperature effects # do not push the system above or below AC clip point. # It's only a reference forecast! clear_ref = pvmodel.irradiance_to_power( site.modeling_parameters, solar_position_obs['apparent_zenith'], solar_position_obs['azimuth'], clearsky_obs['ghi'], clearsky_obs['dni'], clearsky_obs['dhi'] ) clear_fx = pvmodel.irradiance_to_power( site.modeling_parameters, solar_position_fx['apparent_zenith'], solar_position_fx['azimuth'], clearsky_fx['ghi'], clearsky_fx['dni'], clearsky_fx['dhi'] ) else: # assume we are working with ghi, dni, or dhi. clear_ref = clearsky_obs[observation.variable] clear_fx = clearsky_fx[observation.variable] # resample sub-interval reference clear sky to observation intervals clear_ref_resampled = clear_ref.resample( observation.interval_length, closed=closed, label=closed).mean() # calculate persistence index (clear sky index or ac power index) # avg{index_{t_start}...index_{t_end}} = # avg{obs_{t_start}/clear_{t_start}...obs_{t_end}/clear_{t_end}} # clear_ref is calculated at high temporal resolution, so this is accurate # for any observation interval length # apply clip to the clear sky index array before computing the average. # this prevents outliers from distorting the mean, a common occurance # near sunrise and sunset. pers_index = (obs / clear_ref_resampled).clip(lower=0, upper=2).mean() # average instantaneous clear forecasts over interval_length windows # resample operation should be safe due to # _check_interval_length calls above clear_fx_resampled = clear_fx.resample( interval_length, closed=closed_fx, label=closed_fx).mean() # finally, make forecast # fx_t_f = avg{index_{t_start}...index_{t_end}} * clear_t_f fx = pers_index * clear_fx_resampled return fx
timezone='America/Denver', provider='Sandia', modeling_parameters=modeling_params) times = pd.date_range(start='20190801', end='20191001', freq='5min', closed='left') solpos = pvmodel.calculate_solar_position(metadata.latitude, metadata.longitude, metadata.elevation, times) cs = pvmodel.calculate_clearsky(metadata.latitude, metadata.longitude, metadata.elevation, solpos['apparent_zenith']) ac_clear = pvmodel.irradiance_to_power(modeling_params, solpos['apparent_zenith'], solpos['azimuth'], cs['ghi'], cs['dni'], cs['dhi']) ac_clear_1h = ac_clear.resample('1h').mean() ac_clear_1h = pd.DataFrame({'value': ac_clear_1h, 'quality_flag': 0}) ac_clear_1h.to_csv('boulder_clear_aug_sept.csv', index_label='timestamp') # make it cloudy, but apply clipping random = np.random.random(size=len(times)) + 0.5 random = random.clip(max=1) ac = ac_clear * random ac_1h = ac.resample('1h').mean() ac_1h = pd.DataFrame({'value': ac_1h, 'quality_flag': 0}) ac_1h.to_csv('boulder_cloudy_aug_sept.csv', index_label='timestamp')