def _clipping_power(ac_power, slope_max, power_min, power_quantile, freq=None): # Calculate a power threshold, above which the power is being # clipped. # # - The daytime power curve is calculated using # _daytime_powercurve(). This function groups `ac_power` by # minute of the day and returns the `power_quantile`-quantile of # power for each minute. # # - Each timestamp in the daytime power curve that satisfies the # clipping criteria[*] is flagged. # # - The clipping threshold is calculated as the mean power during the # longest flagged period in the daytime power curve. # # [*] clipping criteria: a timestamp satisfies the clipping # criteria if the absolute value of the slope of the daytime power curve # is less than `slope_max` and the value of the daytime # power curve is greater than `power_min` times the median of the # daytime power curve. # # Based on the PVFleets QA Analysis project if not freq: freq = util.freq_to_timedelta(pd.infer_freq( ac_power.index)).seconds / 60 elif isinstance(freq, str): freq = util.freq_to_timedelta(freq).seconds / 60 # Use the slope of the 99.5% quantile of daytime power at # each minute to identify clipping. powercurve = _daytime_powercurve(ac_power, power_quantile) normalized_power = powercurve / powercurve.max() power_slope = (normalized_power.diff() / normalized_power.index.to_series().diff()) * freq clipped_times = _clipped(powercurve, power_slope, powercurve.median() * power_min, slope_max) clipping_cumsum = (~clipped_times).cumsum() # get the value of the cumulative sum of the longest True span longest_clipped = clipping_cumsum.value_counts().idxmax() # select the longest span that satisfies the clipping criteria longest = powercurve[clipping_cumsum == longest_clipped] if longest.index.max() - longest.index.min() >= 60: # if the period of clipping is at least 60 minutes then we # have enough evidence to determine the clipping threshold. return longest.mean() return None
def _freq_minutes(index, freq): """Return the frequency in minutes for `freq`. If `freq` is None then use the frequency inferred from `index`.""" if freq is None: freq = pd.infer_freq(index) if freq is None: raise ValueError("cannot infer frequency") return util.freq_to_timedelta(freq).seconds / 60
def fixed(ghi, daytime, clearsky, interval=None, min_gradient=2): """Detects shadows from fixed structures such as wires and poles. Uses morphological image processing methods to identify shadows from fixed local objects in GHI data. GHI data are assumed to be reasonably complete with relatively few missing values and at a fixed time interval nominally of 1 minute over the course of several months. Detection focuses on shadows with relatively short duration. The algorithm forms a 2D image of the GHI data by arranging time of day along the x-axis and day of year along the y-axis. Rapid change in GHI in the x-direction is used to identify edges of shadows; continuity in the y-direction is used to separate local object shading from cloud shadows. Parameters ---------- ghi : Series Time series of GHI measurements. Data must be in local time at 1-minute frequency and should cover at least 60 days. daytime : Series Boolean series with True for times when the sun is up. clearsky : Series Clearsky GHI with same index as `ghi`. interval : int, optional Interval between data points in minutes. If not specified the interval is inferred from the frequency of the index of `ghi`. min_gradient : float, default 2 Threshold value for the morphological gradient [3]_. Returns ------- Series Boolean series with true for times that are impacted by shadows. ndarray A boolean image (black and white) showing the shadows that were detected. References ---------- .. [1] Martin, C. E., Hansen, C. W., An Image Processing Algorithm to Identify Near-Field Shading in Irradiance Measurements, preprint 2016 .. [2] Reno, M.J. and C.W. Hansen, "Identification of periods of clear sky irradiance in time series of GHI measurements" Renewable Energy, v90, p. 520-531, 2016. .. [3] https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.morphological_gradient.html """ # noqa: E501 if interval is None: interval = util.freq_to_timedelta(pd.infer_freq( ghi.index)).seconds // 60 if interval != 1: raise ValueError("Data must be at 1-minute intervals") ghi_image, clearsky_image, clouds_image, index = _prepare_images( ghi, clearsky, daytime, interval) # normalize the GHI and dampen the dynamic range where the clear # sky model may have large errors (e.g. at very low sun elevation) alpha = 2000 ghi_boosted = 1000 * (ghi_image + alpha) / (clearsky_image + alpha) # We must use scipy.ndimage here because skimage does not support # floating point data outside the range [-1, 1]. gradient = ndimage.morphological_gradient(ghi_boosted, size=(1, 3)) threshold = gradient > min_gradient # binary image of wire candidates # From here we CAN use skimage because we are working with binary images. three_minute_mask = morphology.rectangle(1, 3) wires = morphology.remove_small_objects( morphology.binary_closing(threshold, three_minute_mask), min_size=200, connectivity=2 # all neighbors (including diagonals) ) wires_image = _clean_wires(wires) wires_series = _to_series(wires, index) wires_series = wires_series.reindex(ghi.index, fill_value=False) return wires_series, wires_image
def _freqstr_to_hours(freq): # Convert pandas freqstr to hours (as a float) return util.freq_to_timedelta(freq).seconds / 3600
def _to_hours(freqstr): return util.freq_to_timedelta(freqstr).seconds / 3600
def _freq_to_seconds(freq): delta = util.freq_to_timedelta(freq) return delta.days * (1440 * 60) + delta.seconds
def _freqstr_to_minutes(freqstr): return util.freq_to_timedelta(freqstr).seconds / 60