Skip to content

Commit 36ee5bb

Browse files
Merge branch 'fix_issue_#370' of https://github.com/NREL/rdtools into fix_issue_#370
2 parents d6c40ac + b376fc8 commit 36ee5bb

File tree

9 files changed

+259
-260
lines changed

9 files changed

+259
-260
lines changed

CITATION.cff

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
cff-version: 1.2.0
2+
message: "If you use this software, please cite it as below."
3+
authors:
4+
- family-names: "Deceglie"
5+
given-names: "Michael G."
6+
orcid: "https://orcid.org/0000-0001-7063-9676"
7+
- family-names: "Anderson"
8+
given-names: "Kevin"
9+
- family-names: "Shinn"
10+
given-names: "Adam"
11+
- family-names: "Ambarish"
12+
given-names: "Nag"
13+
- family-names: "Mikofski"
14+
given-names: "Mark"
15+
orcid: "https://orcid.org/0000-0001-8001-8582"
16+
- family-names: "Springer"
17+
given-names: "Martin"
18+
orcid: "https://orcid.org/0000-0001-6803-108X"
19+
- family-names: "Yan"
20+
given-names: "Jiyang"
21+
- family-names: "Perry"
22+
given-names: "Kirsten"
23+
- family-names: "Villamar"
24+
given-names: "Sandra"
25+
- family-names: "Vining"
26+
given-names: "Will"
27+
- family-names: "Kimball"
28+
given-names: "Gregory M."
29+
orcid: "https://orcid.org/0000-0003-1075-1417"
30+
- family-names: "Ruth"
31+
given-names: "Daniel"
32+
- family-names: "Moyer"
33+
given-names: "Noah"
34+
- family-names: "Nguyen"
35+
given-names: "Quyen"
36+
- family-names: "Jordan"
37+
given-names: "Dirk"
38+
orcid: "https://orcid.org/0000-0002-2183-7489"
39+
- family-names: "Muller"
40+
given-names: "Matthew"
41+
- family-names: "Deline"
42+
given-names: "Chris"
43+
orcid: "https://orcid.org/0000-0002-9867-8930"
44+
title: "RdTools"
45+
doi: 10.5281/zenodo.1210316
46+
url: "https://github.com/NREL/rdtools"

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ RdTools is an open-source library to support reproducible technical analysis of
1313
time series data from photovoltaic energy systems. The library aims to provide
1414
best practice analysis routines along with the building blocks for users to
1515
tailor their own analyses. Current applications include the evaluation of PV
16-
production over several years to obtain rates of performance degradation and
16+
production over several years to obtain rates of performance degradation and
1717
soiling loss. RdTools can handle both high frequency (hourly or better) or low
1818
frequency (daily, weekly, etc.) datasets. Best results are obtained with higher
1919
frequency data.
@@ -34,10 +34,10 @@ RdTools currently is tested on Python 3.9+.
3434
To cite RdTools, please use the following along with the version number
3535
and the specific DOI coresponding to that version from [Zenodo](https://doi.org/10.5281/zenodo.1210316):
3636

37-
- Michael G. Deceglie, Ambarish Nag, Adam Shinn, Gregory Kimball,
38-
Daniel Ruth, Dirk Jordan, Jiyang Yan, Kevin Anderson, Kirsten Perry,
39-
Mark Mikofski, Matthew Muller, Will Vining, and Chris Deline,
40-
RdTools, version {insert version}, Computer Software,
37+
- Michael G. Deceglie, Kevin Anderson, Adam Shinn, Ambarish Nag, Mark Mikofski,
38+
Martin Springer, Jiyang Yan, Kirsten Perry, Sandra Villamar, Will Vining,
39+
Gregory Kimball, Daniel Ruth, Noah Moyer, Quyen Nguyen, Dirk Jordan,
40+
Matthew Muller, and Chris Deline, RdTools, version {insert version}, Computer Software,
4141
https://github.com/NREL/rdtools. DOI:{insert DOI}
4242

4343
The underlying workflow of RdTools has been published in several places.

docs/TrendAnalysis_example.ipynb

Lines changed: 13 additions & 82 deletions
Large diffs are not rendered by default.

docs/TrendAnalysis_example_NSRDB.ipynb

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

docs/degradation_and_soiling_example.ipynb

Lines changed: 14 additions & 163 deletions
Large diffs are not rendered by default.

docs/sphinx/source/changelog/pending.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ Bug fixes
1212
---------
1313
* Set marker linewidth to zero in `rdtools.plotting.degradation_summary_plots` (:pull:`433`)
1414
* Fix :py:func:`~rdtools.normalization.energy_from_power` returns incorrect index for shifted hourly data (:issue:`370`, :pull:`437`)
15-

rdtools/analysis_chains.py

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,80 @@ def __init__(
155155
self.max_timedelta = max_timedelta
156156
self.results = {}
157157

158-
# Initialize to use default filter parameters
159-
self.filter_params = {
158+
# Define valid filter parameters
159+
self.valid_filter_params = [
160+
"normalized_filter",
161+
"poa_filter",
162+
"tcell_filter",
163+
"clip_filter",
164+
"hour_angle_filter",
165+
"clearsky_filter",
166+
"sensor_clearsky_filter",
167+
"ad_hoc_filter",
168+
]
169+
170+
self.valid_filter_params_aggregated = [
171+
"two_way_window_filter",
172+
"insolation_filter",
173+
"hampel_filter",
174+
"directional_tukey_filter",
175+
"ad_hoc_filter",
176+
]
177+
178+
# Define default filter parameters
179+
self.default_filter_params = {
160180
"normalized_filter": {},
161181
"poa_filter": {},
162182
"tcell_filter": {},
163183
"clip_filter": {},
164184
"clearsky_filter": {},
165185
"ad_hoc_filter": None, # use this to include an explict filter
166186
}
167-
self.filter_params_aggregated = {
187+
188+
self.default_filter_params_aggregated = {
168189
"two_way_window_filter": {},
169-
"ad_hoc_filter": None
190+
"ad_hoc_filter": None,
170191
}
192+
193+
# Initialize to use default filter parameters
194+
self._filter_params = ValidatedFilterDict(
195+
self.valid_filter_params, self.default_filter_params
196+
)
197+
self._filter_params_aggregated = ValidatedFilterDict(
198+
self.valid_filter_params_aggregated, self.default_filter_params_aggregated
199+
)
171200
# remove tcell_filter from list if power_expected is passed in
172201
if power_expected is not None and temperature_cell is None:
173202
del self.filter_params["tcell_filter"]
174203

204+
@property
205+
def filter_params(self):
206+
return self._filter_params
207+
208+
@filter_params.setter
209+
def filter_params(self, new_filter_params):
210+
if not isinstance(new_filter_params, dict):
211+
raise ValueError("Attribute `filter_params` must be a dictionary.")
212+
213+
# If dictionary passed, check the new filter_params and set new filters.
214+
self._filter_params = ValidatedFilterDict(self.valid_filter_params, new_filter_params)
215+
print(f"Attribute `filter_params` changed to: {new_filter_params}")
216+
217+
@property
218+
def filter_params_aggregated(self):
219+
return self._filter_params_aggregated
220+
221+
@filter_params_aggregated.setter
222+
def filter_params_aggregated(self, new_filter_params_aggregated):
223+
if not (isinstance(new_filter_params_aggregated, dict) or None):
224+
raise ValueError("Attribute `filter_params_aggregated` must be a dictionary.")
225+
226+
# If dictionary passed, check the new filter_params and set new filters.
227+
self._filter_params_aggregated = ValidatedFilterDict(
228+
self.valid_filter_params_aggregated, new_filter_params_aggregated
229+
)
230+
print(f"Attribute `filter_params_aggregated` changed to: {new_filter_params_aggregated}")
231+
175232
def set_clearsky(
176233
self,
177234
pvlib_location=None,
@@ -1205,3 +1262,27 @@ def plot_degradation_timeseries(self, case, rolling_days=365, **kwargs):
12051262

12061263
fig = plotting.degradation_timeseries_plot(yoy_info, rolling_days, **kwargs)
12071264
return fig
1265+
1266+
1267+
class ValidatedFilterDict(dict):
1268+
def __init__(self, valid_keys, *args, **kwargs):
1269+
self.valid_keys = valid_keys
1270+
self._err_msg = "Key '{0}' is not a valid filter parameter."
1271+
super(ValidatedFilterDict, self).__init__(*args, **kwargs)
1272+
self._validate_keys()
1273+
1274+
def __setitem__(self, key, value):
1275+
if key not in self.valid_keys:
1276+
raise KeyError(self._err_msg.format(key))
1277+
super(ValidatedFilterDict, self).__setitem__(key, value)
1278+
1279+
def update(self, *args, **kwargs):
1280+
for key in dict(*args, **kwargs).keys():
1281+
if key not in self.valid_keys:
1282+
raise KeyError(self._err_msg.format(key))
1283+
super(ValidatedFilterDict, self).update(*args, **kwargs)
1284+
1285+
def _validate_keys(self):
1286+
for key in self.keys():
1287+
if key not in self.valid_keys:
1288+
raise KeyError(self._err_msg.format(key))

rdtools/plotting.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ def degradation_summary_plots(yoy_rd, yoy_ci, yoy_info, normalized_yield,
112112
colors = yoy_info['usage_of_points'].map({0: 'red', 1: 'green', 2: plot_color})
113113
else:
114114
colors = plot_color
115-
ax1.scatter(renormalized_yield.index, renormalized_yield,
116-
c=colors, alpha=scatter_alpha)
115+
ax1.scatter(
116+
renormalized_yield.index, renormalized_yield, c=colors, alpha=scatter_alpha, linewidths=0
117+
)
117118

118119
ax1.plot(x, y, 'k--', linewidth=3)
119120
ax1.set_xlabel('Date')

rdtools/test/analysis_chains_test.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from rdtools import TrendAnalysis, normalization, filtering
22
from conftest import assert_isinstance, assert_warnings
3+
from rdtools.analysis_chains import ValidatedFilterDict
34
import pytest
45
import pvlib
56
import pandas as pd
@@ -758,3 +759,92 @@ def test_energy_from_power_shifted_hourly_data():
758759

759760
energy = normalization.energy_from_power(pv)
760761
pd.testing.assert_series_equal(energy, pv[1:], check_names=False)
762+
763+
def test_validated_filter_dict_initialization():
764+
valid_keys = ["key1", "key2"]
765+
filter_dict = ValidatedFilterDict(valid_keys, key1="value1", key2="value2")
766+
assert filter_dict["key1"] == "value1"
767+
assert filter_dict["key2"] == "value2"
768+
769+
770+
def test_validated_filter_dict_invalid_key_initialization():
771+
valid_keys = ["key1", "key2"]
772+
with pytest.raises(KeyError, match="Key 'key3' is not a valid filter parameter."):
773+
ValidatedFilterDict(valid_keys, key1="value1", key3="value3")
774+
775+
776+
def test_validated_filter_dict_setitem():
777+
valid_keys = ["key1", "key2"]
778+
filter_dict = ValidatedFilterDict(valid_keys)
779+
filter_dict["key1"] = "value1"
780+
assert filter_dict["key1"] == "value1"
781+
782+
783+
def test_validated_filter_dict_setitem_invalid_key():
784+
valid_keys = ["key1", "key2"]
785+
filter_dict = ValidatedFilterDict(valid_keys)
786+
with pytest.raises(KeyError, match="Key 'key3' is not a valid filter parameter."):
787+
filter_dict["key3"] = "value3"
788+
789+
790+
def test_validated_filter_dict_update():
791+
valid_keys = ["key1", "key2"]
792+
filter_dict = ValidatedFilterDict(valid_keys)
793+
filter_dict.update({"key1": "value1", "key2": "value2"})
794+
assert filter_dict["key1"] == "value1"
795+
assert filter_dict["key2"] == "value2"
796+
797+
798+
def test_validated_filter_dict_update_invalid_key():
799+
valid_keys = ["key1", "key2"]
800+
filter_dict = ValidatedFilterDict(valid_keys)
801+
with pytest.raises(KeyError, match="Key 'key3' is not a valid filter parameter."):
802+
filter_dict.update({"key1": "value1", "key3": "value3"})
803+
804+
805+
@pytest.mark.parametrize(
806+
"filter_param",
807+
[
808+
"normalized_filter",
809+
"poa_filter",
810+
"tcell_filter",
811+
"clip_filter",
812+
"hour_angle_filter",
813+
"clearsky_filter",
814+
"sensor_clearsky_filter",
815+
"ad_hoc_filter",
816+
],
817+
)
818+
def test_valid_filter_params(sensor_analysis, filter_param):
819+
sensor_analysis.filter_params[filter_param] = {}
820+
assert filter_param in sensor_analysis.filter_params
821+
822+
823+
def test_invalid_filter_params(sensor_analysis, filter_param="invalid_filter"):
824+
with pytest.raises(KeyError, match=f"Key '{filter_param}' is not a valid filter parameter."):
825+
sensor_analysis.filter_params[filter_param] = {}
826+
827+
828+
@pytest.mark.parametrize(
829+
"filter_param_aggregated",
830+
[
831+
"two_way_window_filter",
832+
"insolation_filter",
833+
"hampel_filter",
834+
"directional_tukey_filter",
835+
"ad_hoc_filter",
836+
],
837+
)
838+
def test_valid_filter_params_aggregated(sensor_analysis, filter_param_aggregated):
839+
sensor_analysis.filter_params_aggregated[filter_param_aggregated] = {}
840+
assert filter_param_aggregated in sensor_analysis.filter_params_aggregated
841+
842+
843+
def test_invalid_filter_params_aggregated(
844+
sensor_analysis, filter_param_aggregated="invalid_filter"
845+
):
846+
with pytest.raises(
847+
KeyError, match=f"Key '{filter_param_aggregated}' is not a valid filter parameter."
848+
):
849+
sensor_analysis.filter_params_aggregated[filter_param_aggregated] = {}
850+

0 commit comments

Comments
 (0)