Skip to content

Commit e762e8a

Browse files
ajonesrkandersolar
andauthored
Infer thresholds for detect_clearsky (#1784)
* Add helper function to interpolate thresholds * Add infer_limits functionality in detect_clearsky * Update comments/docstrings * Add ValueError message * Fix typo * Update docstring * Add tests, update whatsnew * Update whatsnew * Add mocker test * Update whatsnew * whatsnew fix * Fix test error * Add check on length of data * Added check that optimizer succeeded * Handling output for old version of scipy * Update pvlib/clearsky.py Co-authored-by: Kevin Anderson <[email protected]> * Update pvlib/tests/test_clearsky.py Co-authored-by: Kevin Anderson <[email protected]> * Update pvlib/clearsky.py Co-authored-by: Kevin Anderson <[email protected]> * Update pvlib/clearsky.py Co-authored-by: Kevin Anderson <[email protected]> * Add suggestions from code review --------- Co-authored-by: Kevin Anderson <[email protected]>
1 parent 996361d commit e762e8a

File tree

4 files changed

+253
-3
lines changed

4 files changed

+253
-3
lines changed

docs/sphinx/source/whatsnew/v0.10.2.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Enhancements
1515
:py:func:`pvlib.iotools.get_pvgis_hourly`, :py:func:`pvlib.iotools.get_cams`,
1616
:py:func:`pvlib.iotools.get_bsrn`, and :py:func:`pvlib.iotools.read_midc_raw_data_from_nrel`.
1717
(:pull:`1800`)
18+
* Added option to infer threshold values for
19+
:py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`)
1820

1921

2022
Bug fixes
@@ -39,4 +41,5 @@ Requirements
3941
Contributors
4042
~~~~~~~~~~~~
4143
* Adam R. Jensen (:ghuser:`AdamRJensen`)
44+
* Abigail Jones (:ghuser:`ajonesr`)
4245
* Taos Transue (:ghuser:`reepoi`)

pvlib/clearsky.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,38 @@ def _clear_sample_index(clear_windows, samples_per_window, align, H):
644644
return clear_samples
645645

646646

647-
def detect_clearsky(measured, clearsky, times=None, window_length=10,
648-
mean_diff=75, max_diff=75,
647+
def _clearsky_get_threshold(sample_interval):
648+
"""
649+
Returns threshold values for kwargs in detect_clearsky. See
650+
Table 1 in [1].
651+
652+
References
653+
----------
654+
.. [1] Jordan, D.C. and C. Hansen, "Clear-sky detection for PV
655+
degradation analysis using multiple regression", Renewable Energy,
656+
v209, p. 393-400, 2023.
657+
"""
658+
659+
if (sample_interval < 1 or sample_interval > 30):
660+
raise ValueError("`infer_limits=True` can only be used for inputs \
661+
with time step from 1 to 30 minutes")
662+
663+
data_freq = np.array([1, 5, 15, 30])
664+
665+
window_length = np.interp(sample_interval, data_freq, [50, 60, 90, 120])
666+
mean_diff = np.interp(sample_interval, data_freq, [75, 75, 75, 75])
667+
max_diff = np.interp(sample_interval, data_freq, [60, 65, 75, 90])
668+
lower_line_length = np.interp(sample_interval, data_freq, [-45,-45,-45,-45])
669+
upper_line_length = np.interp(sample_interval, data_freq, [80, 80, 80, 80])
670+
var_diff = np.interp(sample_interval, data_freq, [0.005, 0.01, 0.032, 0.07])
671+
slope_dev = np.interp(sample_interval, data_freq, [50, 60, 75, 96])
672+
673+
return (window_length, mean_diff, max_diff, lower_line_length,
674+
upper_line_length, var_diff, slope_dev)
675+
676+
677+
def detect_clearsky(measured, clearsky, times=None, infer_limits=False,
678+
window_length=10, mean_diff=75, max_diff=75,
649679
lower_line_length=-5, upper_line_length=10,
650680
var_diff=0.005, slope_dev=8, max_iterations=20,
651681
return_components=False):
@@ -676,6 +706,9 @@ def detect_clearsky(measured, clearsky, times=None, window_length=10,
676706
times : DatetimeIndex or None, default None.
677707
Times of measured and clearsky values. If None the index of measured
678708
will be used.
709+
infer_limits : bool, default False
710+
If True, does not use passed in kwargs (or defaults), but instead
711+
interpolates these values from Table 1 in [2]_.
679712
window_length : int, default 10
680713
Length of sliding time window in minutes. Must be greater than 2
681714
periods.
@@ -731,6 +764,9 @@ def detect_clearsky(measured, clearsky, times=None, window_length=10,
731764
.. [1] Reno, M.J. and C.W. Hansen, "Identification of periods of clear
732765
sky irradiance in time series of GHI measurements" Renewable Energy,
733766
v90, p. 520-531, 2016.
767+
.. [2] Jordan, D.C. and C. Hansen, "Clear-sky detection for PV
768+
degradation analysis using multiple regression", Renewable Energy,
769+
v209, p. 393-400, 2023. :doi:`10.1016/j.renene.2023.04.035`
734770
735771
Notes
736772
-----
@@ -773,6 +809,21 @@ def detect_clearsky(measured, clearsky, times=None, window_length=10,
773809
sample_interval, samples_per_window = \
774810
tools._get_sample_intervals(times, window_length)
775811

812+
# if infer_limits, find threshold values using the sample interval
813+
if infer_limits:
814+
window_length, mean_diff, max_diff, lower_line_length, \
815+
upper_line_length, var_diff, slope_dev = \
816+
_clearsky_get_threshold(sample_interval)
817+
818+
# recalculate samples_per_window using returned window_length
819+
_, samples_per_window = \
820+
tools._get_sample_intervals(times, window_length)
821+
822+
# check that we have enough data to produce a nonempty hankel matrix
823+
if len(times) < samples_per_window:
824+
raise ValueError(f"times has only {len(times)} entries, but it must \
825+
have at least {samples_per_window} entries")
826+
776827
# generate matrix of integers for creating windows with indexing
777828
H = hankel(np.arange(samples_per_window),
778829
np.arange(samples_per_window-1, len(times)))
@@ -826,7 +877,22 @@ def detect_clearsky(measured, clearsky, times=None, window_length=10,
826877
def rmse(alpha):
827878
return np.sqrt(np.mean((clear_meas - alpha*clear_clear)**2))
828879

829-
alpha = minimize_scalar(rmse).x
880+
optimize_result = minimize_scalar(rmse)
881+
if not optimize_result.success:
882+
try:
883+
message = "Optimizer exited unsuccessfully: " \
884+
+ optimize_result.message
885+
except AttributeError:
886+
message = "Optimizer exited unsuccessfully: \
887+
No message explaining the failure was returned. \
888+
If you would like to see this message, please \
889+
update your scipy version (try version 1.8.0 \
890+
or beyond)."
891+
raise RuntimeError(message)
892+
893+
else:
894+
alpha = optimize_result.x
895+
830896
if round(alpha*10000) == round(previous_alpha*10000):
831897
break
832898
else:
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# latitude:35.04
2+
# longitude:-106.62
3+
# elevation:1619
4+
# window_length:10
5+
Time (UTC),GHI,Clear or not
6+
4/1/2012 17:30,862.0935267144818,1
7+
4/1/2012 17:31,863.9568654037691,1
8+
4/1/2012 17:32,865.802767695237,1
9+
4/1/2012 17:33,867.6311939211416,1
10+
4/1/2012 17:34,869.4421084958154,1
11+
4/1/2012 17:35,871.2354749580768,1
12+
4/1/2012 17:36,873.0112572200139,1
13+
4/1/2012 17:37,874.7694195555663,1
14+
4/1/2012 17:38,876.5099266138993,1
15+
4/1/2012 17:39,878.2327434081394,1
16+
4/1/2012 17:40,879.9378353205582,1
17+
4/1/2012 17:41,881.6251681074431,1
18+
4/1/2012 17:42,500.0,0
19+
4/1/2012 17:43,300.0,0
20+
4/1/2012 17:44,400.0,0
21+
4/1/2012 17:45,888.1962370575133,1
22+
4/1/2012 17:46,889.7942734332919,1
23+
4/1/2012 17:47,891.3743529660121,1
24+
4/1/2012 17:48,892.9364440027008,1
25+
4/1/2012 17:49,894.4805152569894,1
26+
4/1/2012 17:50,896.0065358138269,1
27+
4/1/2012 17:51,897.514475133874,1
28+
4/1/2012 17:52,899.0043030438522,1
29+
4/1/2012 17:53,900.4759897479845,1
30+
4/1/2012 17:54,901.9295058184756,1
31+
4/1/2012 17:55,903.3648231563722,1
32+
4/1/2012 17:56,950.0,0
33+
4/1/2012 17:57,906.1807424805852,1
34+
4/1/2012 17:58,907.5612891914101,1
35+
4/1/2012 17:59,908.9235237269177,1
36+
4/1/2012 18:00,910.2674188971979,1
37+
4/1/2012 18:01,911.592947889838,1
38+
4/1/2012 18:02,912.9000842614388,1
39+
4/1/2012 18:03,914.1888019477013,1
40+
4/1/2012 18:04,915.4590752551169,1
41+
4/1/2012 18:05,916.7108788648916,1
42+
4/1/2012 18:06,917.9441886574208,1
43+
4/1/2012 18:07,919.1589784086842,1
44+
4/1/2012 18:08,920.3552247618425,1
45+
4/1/2012 18:09,921.5329038989687,1
46+
4/1/2012 18:10,922.691992377965,1
47+
4/1/2012 18:11,923.8324671359268,1
48+
4/1/2012 18:12,924.9543054818693,1
49+
4/1/2012 18:13,926.057485105408,1
50+
4/1/2012 18:14,927.141984069634,1
51+
4/1/2012 18:15,928.2077808145282,1
52+
4/1/2012 18:16,929.254854160055,1
53+
4/1/2012 18:17,930.2831839827653,1
54+
4/1/2012 18:18,931.2927484780441,1
55+
4/1/2012 18:19,932.2835282910601,1
56+
4/1/2012 18:20,933.2555037490038,1
57+
4/1/2012 18:21,934.2086555597849,1
58+
4/1/2012 18:22,935.1429648059466,1
59+
4/1/2012 18:23,936.058412951919,1
60+
4/1/2012 18:24,936.9549818381174,1
61+
4/1/2012 18:25,937.8326536837798,1
62+
4/1/2012 18:26,938.6914110895166,1
63+
4/1/2012 18:27,939.5312370318452,1
64+
4/1/2012 18:28,940.3521148697101,1
65+
4/1/2012 18:29,941.1540288705581,1
66+
4/1/2012 18:30,941.9369620746523,1
67+
4/1/2012 18:31,942.7008995238247,1
68+
4/1/2012 18:32,943.4458260928685,1
69+
4/1/2012 18:33,944.1717270394992,1
70+
4/1/2012 18:34,944.8785879996012,1
71+
4/1/2012 18:35,945.5663949895419,1
72+
4/1/2012 18:36,946.2351344081491,1
73+
4/1/2012 18:37,946.8847930330305,1
74+
4/1/2012 18:38,947.5153580226918,1
75+
4/1/2012 18:39,948.126816918376,1
76+
4/1/2012 18:40,948.7191580309192,1
77+
4/1/2012 18:41,949.2923688693852,1
78+
4/1/2012 18:42,949.8464385206038,1
79+
4/1/2012 18:43,950.3813560493355,1
80+
4/1/2012 18:44,950.8971109033968,1
81+
4/1/2012 18:45,951.3936929103834,1
82+
4/1/2012 18:46,951.8710922815745,1
83+
4/1/2012 18:47,952.3292996087865,1
84+
4/1/2012 18:48,952.7683058659376,1
85+
4/1/2012 18:49,953.1881024102506,1
86+
4/1/2012 18:50,953.5886809795984,1
87+
4/1/2012 18:51,953.9700339452911,1
88+
4/1/2012 18:52,954.3321532981175,1
89+
4/1/2012 18:53,954.6750321861093,1
90+
4/1/2012 18:54,954.9986638782349,1
91+
4/1/2012 18:55,955.3030420248847,1
92+
4/1/2012 18:56,955.5881606602495,1
93+
4/1/2012 18:57,955.8540142004646,1
94+
4/1/2012 18:58,956.1005974445337,1
95+
4/1/2012 18:59,956.3279055749699,1
96+
4/1/2012 19:00,956.5359341563872,1
97+
4/1/2012 19:01,956.7246791371283,1
98+
4/1/2012 19:02,956.8941369551687,1
99+
4/1/2012 19:03,957.0443040971436,1
100+
4/1/2012 19:04,957.175177780536,1
101+
4/1/2012 19:05,957.2867554853715,1
102+
4/1/2012 19:06,957.3790350752402,1
103+
4/1/2012 19:07,957.4520147966049,1
104+
4/1/2012 19:08,957.5056932791584,1
105+
4/1/2012 19:09,957.5400695359402,1
106+
4/1/2012 19:10,957.5551429630466,1
107+
4/1/2012 19:11,957.5509133398784,1
108+
4/1/2012 19:12,957.527380828979,1
109+
4/1/2012 19:13,957.4845459762313,1
110+
4/1/2012 19:14,957.4224096624041,1
111+
4/1/2012 19:15,957.3409732831614,1
112+
4/1/2012 19:16,957.2402384985319,1
113+
4/1/2012 19:17,957.1202073871192,1
114+
4/1/2012 19:18,956.9808824107419,1
115+
4/1/2012 19:19,956.8222664139458,1
116+
4/1/2012 19:20,956.6443626248492,1
117+
4/1/2012 19:21,956.4471746540574,1
118+
4/1/2012 19:22,956.2307064955613,1
119+
4/1/2012 19:23,955.9949625264852,1
120+
4/1/2012 19:24,955.7399475061342,1
121+
4/1/2012 19:25,955.4656663871413,1
122+
4/1/2012 19:26,955.1721250622959,1
123+
4/1/2012 19:27,954.8593292624192,1
124+
4/1/2012 19:28,954.5272852786308,1
125+
4/1/2012 19:29,954.175999783701,1
126+
4/1/2012 19:30,953.8054798341012,1

pvlib/tests/test_clearsky.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,49 @@ def detect_clearsky_data():
533533
return expected, cs
534534

535535

536+
@pytest.fixture
537+
def detect_clearsky_threshold_data():
538+
# this is (roughly) just a 2 hour period of the same data in
539+
# detect_clearsky_data (which only spans 30 minutes)
540+
data_file = DATA_DIR / 'detect_clearsky_threshold_data.csv'
541+
expected = pd.read_csv(
542+
data_file, index_col=0, parse_dates=True, comment='#')
543+
expected = expected.tz_localize('UTC').tz_convert('Etc/GMT+7')
544+
metadata = {}
545+
with data_file.open() as f:
546+
for line in f:
547+
if line.startswith('#'):
548+
key, value = line.strip('# \n').split(':')
549+
metadata[key] = float(value)
550+
else:
551+
break
552+
metadata['window_length'] = int(metadata['window_length'])
553+
loc = Location(metadata['latitude'], metadata['longitude'],
554+
altitude=metadata['elevation'])
555+
# specify turbidity to guard against future lookup changes
556+
cs = loc.get_clearsky(expected.index, linke_turbidity=2.658197)
557+
return expected, cs
558+
559+
560+
def test_clearsky_get_threshold():
561+
out = clearsky._clearsky_get_threshold(4.5)
562+
expected = (58.75, 75, 64.375, -45, 80.0, 0.009375, 58.75)
563+
assert np.allclose(out, expected)
564+
565+
566+
def test_clearsky_get_threshold_raises_error():
567+
with pytest.raises(ValueError, match='can only be used for inputs'):
568+
clearsky._clearsky_get_threshold(0.5)
569+
570+
571+
def test_detect_clearsky_calls_threshold(mocker, detect_clearsky_threshold_data):
572+
threshold_spy = mocker.spy(clearsky, '_clearsky_get_threshold')
573+
expected, cs = detect_clearsky_threshold_data
574+
threshold_actual = clearsky.detect_clearsky(expected['GHI'], cs['ghi'],
575+
infer_limits=True)
576+
assert threshold_spy.call_count == 1
577+
578+
536579
def test_detect_clearsky(detect_clearsky_data):
537580
expected, cs = detect_clearsky_data
538581
clear_samples = clearsky.detect_clearsky(
@@ -629,6 +672,18 @@ def test_detect_clearsky_missing_index(detect_clearsky_data):
629672
clearsky.detect_clearsky(expected['GHI'].values, cs['ghi'].values)
630673

631674

675+
def test_detect_clearsky_not_enough_data(detect_clearsky_data):
676+
expected, cs = detect_clearsky_data
677+
with pytest.raises(ValueError, match='have at least'):
678+
clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=60)
679+
680+
681+
def test_detect_clearsky_optimizer_failed(detect_clearsky_data):
682+
expected, cs = detect_clearsky_data
683+
with pytest.raises(RuntimeError, match='Optimizer exited unsuccessfully'):
684+
clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=15)
685+
686+
632687
@pytest.fixture
633688
def detect_clearsky_helper_data():
634689
samples_per_window = 3

0 commit comments

Comments
 (0)