Skip to content

Commit 3721a4d

Browse files
authored
Merge pull request #436 from NREL/fix_issue_#377
Add checks to filter arguments for TrendAnalysis (Issue #377)
2 parents 58b00e1 + 2479e35 commit 3721a4d

File tree

3 files changed

+176
-4
lines changed

3 files changed

+176
-4
lines changed

docs/sphinx/source/changelog/pending.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ v3.0.0 (December XX, 2024)
55
Enhancements
66
------------
77
* Add `CITATION.cff` file for citation information (:pull:`434`)
8+
* Added checks to TrendAnalysis for `filter_params` and `filter_params_aggregated`. Raises an error if unkown filter is supplied. (:pull:`436`)
89

910

1011
Bug fixes

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/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
@@ -740,3 +741,92 @@ def test_plot_degradation_timeseries(sensor_analysis, clearsky_analysis):
740741
assert_isinstance(
741742
clearsky_analysis.plot_degradation_timeseries("clearsky"), plt.Figure
742743
)
744+
745+
746+
def test_validated_filter_dict_initialization():
747+
valid_keys = ["key1", "key2"]
748+
filter_dict = ValidatedFilterDict(valid_keys, key1="value1", key2="value2")
749+
assert filter_dict["key1"] == "value1"
750+
assert filter_dict["key2"] == "value2"
751+
752+
753+
def test_validated_filter_dict_invalid_key_initialization():
754+
valid_keys = ["key1", "key2"]
755+
with pytest.raises(KeyError, match="Key 'key3' is not a valid filter parameter."):
756+
ValidatedFilterDict(valid_keys, key1="value1", key3="value3")
757+
758+
759+
def test_validated_filter_dict_setitem():
760+
valid_keys = ["key1", "key2"]
761+
filter_dict = ValidatedFilterDict(valid_keys)
762+
filter_dict["key1"] = "value1"
763+
assert filter_dict["key1"] == "value1"
764+
765+
766+
def test_validated_filter_dict_setitem_invalid_key():
767+
valid_keys = ["key1", "key2"]
768+
filter_dict = ValidatedFilterDict(valid_keys)
769+
with pytest.raises(KeyError, match="Key 'key3' is not a valid filter parameter."):
770+
filter_dict["key3"] = "value3"
771+
772+
773+
def test_validated_filter_dict_update():
774+
valid_keys = ["key1", "key2"]
775+
filter_dict = ValidatedFilterDict(valid_keys)
776+
filter_dict.update({"key1": "value1", "key2": "value2"})
777+
assert filter_dict["key1"] == "value1"
778+
assert filter_dict["key2"] == "value2"
779+
780+
781+
def test_validated_filter_dict_update_invalid_key():
782+
valid_keys = ["key1", "key2"]
783+
filter_dict = ValidatedFilterDict(valid_keys)
784+
with pytest.raises(KeyError, match="Key 'key3' is not a valid filter parameter."):
785+
filter_dict.update({"key1": "value1", "key3": "value3"})
786+
787+
788+
@pytest.mark.parametrize(
789+
"filter_param",
790+
[
791+
"normalized_filter",
792+
"poa_filter",
793+
"tcell_filter",
794+
"clip_filter",
795+
"hour_angle_filter",
796+
"clearsky_filter",
797+
"sensor_clearsky_filter",
798+
"ad_hoc_filter",
799+
],
800+
)
801+
def test_valid_filter_params(sensor_analysis, filter_param):
802+
sensor_analysis.filter_params[filter_param] = {}
803+
assert filter_param in sensor_analysis.filter_params
804+
805+
806+
def test_invalid_filter_params(sensor_analysis, filter_param="invalid_filter"):
807+
with pytest.raises(KeyError, match=f"Key '{filter_param}' is not a valid filter parameter."):
808+
sensor_analysis.filter_params[filter_param] = {}
809+
810+
811+
@pytest.mark.parametrize(
812+
"filter_param_aggregated",
813+
[
814+
"two_way_window_filter",
815+
"insolation_filter",
816+
"hampel_filter",
817+
"directional_tukey_filter",
818+
"ad_hoc_filter",
819+
],
820+
)
821+
def test_valid_filter_params_aggregated(sensor_analysis, filter_param_aggregated):
822+
sensor_analysis.filter_params_aggregated[filter_param_aggregated] = {}
823+
assert filter_param_aggregated in sensor_analysis.filter_params_aggregated
824+
825+
826+
def test_invalid_filter_params_aggregated(
827+
sensor_analysis, filter_param_aggregated="invalid_filter"
828+
):
829+
with pytest.raises(
830+
KeyError, match=f"Key '{filter_param_aggregated}' is not a valid filter parameter."
831+
):
832+
sensor_analysis.filter_params_aggregated[filter_param_aggregated] = {}

0 commit comments

Comments
 (0)