def fixed_nrel(power_or_irradiance, daytime, r2_min=0.94, min_hours=5, peak_min=None): """Flag days that match the profile of a fixed PV system on a sunny day. This algorithm relies on the observation that the power profile of a fixed tilt PV system often resembles a quadratic polynomial on a sunny day, with a single peak when the sun is near the system azimuth. A day is marked True when the :math:`r^2` for a quadratic fit to the power data is greater than `r2_min`. Parameters ---------- power_or_irradiance : Series Timezone localized series of power or irradiance measurements. daytime : Series Boolean series with True for times that are during the day. For best results this mask should exclude early morning and evening as well as night. Data at these times may have problems with shadows that interfere with curve fitting. r2_min : float, default 0.94 Minimum :math:`r^2` of a quadratic fit for a day to be marked True. min_hours : float, default 5.0 Minimum number of hours with data to attempt a fit on a day. peak_min : float, default None The maximum `power_or_irradiance` value for a day must be greater than `peak_min` for a fit to be attempted. If the maximum for a day is less than `peak_min` then the day will be marked False. Returns ------- Series True for values on days where `power_or_irradiance` matches the expected parabolic profile for a fixed PV system on a sunny day. Notes ----- This algorithm is based on the PVFleets QA Analysis project. Copyright (c) 2020 Alliance for Sustainable Energy, LLC. """ freq = pd.infer_freq(power_or_irradiance.index) daily_data = _group.by_day(power_or_irradiance[daytime]) minutes = pd.Series(power_or_irradiance.index.hour * 60 + power_or_irradiance.index.minute, index=power_or_irradiance.index) fixed_days = daily_data.apply(_conditional_fit, fitfunc=_fit.quadratic_r2, minutes=minutes, freq=freq, min_hours=min_hours, peak_min=peak_min) return (fixed_days > r2_min).reindex(power_or_irradiance.index, method='pad', fill_value=False)
def _peak_times(data): minute_of_day = pd.Series(data.index.hour * 60 + data.index.minute, index=data.index) peak_minutes = _group.by_day(data).apply( lambda day: pd.Timedelta(minutes=round( _fit.quadratic_vertex( x=minute_of_day[day.index], y=day, )))) return pd.DatetimeIndex(np.unique(data.index.date), tz=data.index.tz) + peak_minutes
def tracking_nrel(power_or_irradiance, daytime, r2_min=0.915, r2_fixed_max=0.96, min_hours=5, peak_min=None, quadratic_mask=None): """Flag days that match the profile of a single-axis tracking PV system on a sunny day. This algorithm relies on the observation that the power profile of a single-axis tracking PV system tends to resemble a quartic polynomial on a sunny day, I.e., two peaks are observed, one before and one after the sun crosses the tracker azimuth. By contrast, the power profile for a fixed tilt PV system often resembles a quadratic polynomial on a sunny day, with a single peak when the sun is near the system azimuth. The algorithm fits both a quartic and a quadratic polynomial to each day's data. A day is marked True if the quartic fit has a sufficiently high :math:`r^2` and the quadratic fit has a sufficiently low :math:`r^2`. Specifically, a day is marked True when three conditions are met: 1. a restricted quartic [#]_ must fit the data with :math:`r^2` greater than `r2_min` 2. the :math:`r^2` for the restricted quartic fit must be greater than the :math:`r^2` for a quadratic fit 3. the :math:`r^2` for a quadratic fit must be less than `r2_fixed_max` Values on days where any one of these conditions is not met are marked False. .. [#] The specific quartic used for this fit is centered within 70 minutes of 12:00, the y-value at the center must be within 15% of the median for the day, and it must open downwards. Parameters ---------- power_or_irradiance : Series Timezone localized series of power or irradiance measurements. daytime : Series Boolean series with True for times that are during the day. For best results this mask should exclude early morning and late afternoon as well as night. Data at these times may have problems with shadows that interfere with curve fitting. r2_min : float, default 0.915 Minimum :math:`r^2` of a quartic fit for a day to be marked True. r2_fixed_max : float, default 0.96 If the :math:`r^2` of a quadratic fit exceeds `r2_fixed_max`, then tracking/fixed cannot be distinguished and the day is marked False. min_hours : float, default 5.0 Minimum number of hours with data to attempt a fit on a day. peak_min : float, default None The maximum `power_or_irradiance` value for a day must be greater than `peak_min` for a fit to be attempted. If the maximum for a day is less than `peak_min` then the day will be marked False. quadratic_mask : Series, default None If None then `daytime` is used. This Series is used to remove morning and afternoon times from the data before applying a quadratic fit. The mask should typically exclude more data than `daytime` in order to eliminate long tails in the morning or afternoon that can appear if a tracker is stuck in a West or East orientation. Returns ------- Series Boolean series with True for every value on a day that has a tracking profile (see criteria above). Notes ----- This algorithm is based on the PVFleets QA Analysis project. Copyright (c) 2020 Alliance for Sustainable Energy, LLC. """ if quadratic_mask is None: quadratic_mask = daytime freq = pd.infer_freq(power_or_irradiance.index) minutes = pd.Series( power_or_irradiance.index.hour * 60 + power_or_irradiance.index.minute, index=power_or_irradiance.index ) daily_data = _group.by_day(power_or_irradiance[daytime]) tracking_days = daily_data.apply( _conditional_fit, fitfunc=_fit.quartic_restricted_r2, minutes=minutes, freq=freq, min_hours=min_hours, peak_min=peak_min ) fixed_days = _group.by_day(power_or_irradiance[quadratic_mask]).apply( _conditional_fit, fitfunc=_fit.quadratic_r2, minutes=minutes, freq=freq, min_hours=min_hours, peak_min=peak_min ) return ( (tracking_days > r2_min) & (tracking_days > fixed_days) & (fixed_days < r2_fixed_max) ).reindex(power_or_irradiance.index, method='pad', fill_value=False)
def infer_orientation_daily_peak(power_or_poa, sunny, tilts, azimuths, solar_azimuth, solar_zenith, ghi, dhi, dni): """Determine system azimuth and tilt from power or POA using solar azimuth at the daily peak. The time of the daily peak is estimated by fitting a quadratic to to the data for each day in `power_or_poa` and finding the vertex of the fit. A brute force search is performed on clearsky POA irradiance for all pairs of candidate azimuths and tilts (`azimuths` and `tilts`) to find the pair that results in the closest azimuth to the azimuths calculated at the peak times from the curve fitting step. Closest is determined by minimizing the sum of squared difference between the solar azimuth at the peak time in `power_or_poa` and the solar azimuth at maximum clearsky POA irradiance. The accuracy of the tilt and azimuth returned by this function will vary with the time-resolution of the clearsky and solar position data. For the best accuracy pass `solar_azimuth`, `solar_zenith`, and the clearsky data (`ghi`, `dhi`, and `dni`) with one-minute timestamp spacing. If `solar_azimuth` has timestamp spacing less than one minute it will be resampled and interpolated to estimate azimuth at each minute of the day. Regardless of the timestamp spacing these parameters must cover the same days as `power_or_poa`. Parameters ---------- power_or_poa : Series Timezone localized series of power or POA irradiance measurements. sunny : Series Boolean series with True for values during clearsky conditions. tilts : array-like Candidate tilts in degrees. azimuths : array-like Candidate azimuths in degrees. solar_azimuth : Series Time series of solar azimuth. solar_zenith : Series Time series of solar zenith. ghi : Series Clear sky GHI. dhi : Series Clear sky DHI. dni : Series Clear sky DNI. Returns ------- azimuth : float tilt : float Notes ----- Based on PVFleets QA project. """ peak_times = _peak_times(power_or_poa[sunny]) azimuth_by_minute = solar_azimuth.resample('T').interpolate( method='linear' ) modeled_azimuth = azimuth_by_minute[peak_times] best_azimuth = None best_tilt = None smallest_sse = None for azimuth in azimuths: for tilt in tilts: poa = pvlib.irradiance.get_total_irradiance( tilt, azimuth, solar_zenith, solar_azimuth, ghi=ghi, dhi=dhi, dni=dni ).poa_global poa_azimuths = azimuth_by_minute[ _group.by_day(poa).idxmax() ] filtered_azimuths = poa_azimuths[np.isin( poa_azimuths.index.date, modeled_azimuth.index.date )] sum_of_squares = sum( (filtered_azimuths.values - modeled_azimuth.values)**2 ) if (smallest_sse is None) or (smallest_sse > sum_of_squares): smallest_sse = sum_of_squares best_azimuth = azimuth best_tilt = tilt return best_azimuth, best_tilt