Skip to content

Commit

Permalink
Add missing tests (#413)
Browse files Browse the repository at this point in the history
* add test for hour_angle_filter

* add test_directional_tukey_filter

* add test_insolation_filter

* add test_two_way_window_filter

* add bootstrap additive test

* bootstrap_test fix linting

* run blake

* bootstrap test for value error

* irradiance rescale test for value error

* test for ValueError

* flake8 ignore E203

* add pvlib clearsky filter

* add aggregated filter tests to analysis chain tests

* add missing line

* analysis chain hour angle filter test

* liniting

* sensor_clearsky_filter vs sensor_pvlib_clearsky_filter

* update pandocfilters to 1.5.1

* restrict numpy<2.0

* CODS testing turn on verbose flag

* add test coverage to changelog

* add test for sensor analysis with clearsky filtering

* fix flake8 error

* fix duplicate expected result entry

Co-authored-by: Michael Deceglie <[email protected]>

* unify pytest.raises to with statement convention

---------

Co-authored-by: Michael Deceglie <[email protected]>
Co-authored-by: Michael Deceglie <[email protected]>
  • Loading branch information
3 people authored Jun 27, 2024
1 parent 1f25b2f commit 8d67b24
Show file tree
Hide file tree
Showing 13 changed files with 452 additions and 157 deletions.
5 changes: 4 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
# see https://flake8.pycqa.org/en/latest/user/options.html

[flake8]
# E203 is not PEP8 compliant https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices
# Is excluded from flake8's own config https://flake8.pycqa.org/en/latest/user/configuration.html
extend-ignore = E203
max-line-length = 99
max-doc-length = 99
per-file-ignores =
# rdtools.x.y imported but unused
__init__.py:F401
# invalid escape sequence '\s'
versioneer.py:W605
exclude =
exclude =
docs
.eggs
build
2 changes: 1 addition & 1 deletion docs/notebook_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ nbformat==5.1.0
nest-asyncio==1.5.5
notebook==6.4.12
numexpr==2.10.0
pandocfilters==1.4.2
pandocfilters==1.5.1
parso==0.5.2
pexpect==4.6.0
pickleshare==0.7.5
Expand Down
1 change: 1 addition & 0 deletions docs/sphinx/source/changelog/pending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ when compared with older versions of RdTools
Enhancements
------------
* Added a new wrapper function for clearsky filters (:pull:`412`)
* Improve test coverage, especially for the newly added filter capabilities (:pull:`413`)

Bug fixes
---------
Expand Down
5 changes: 5 additions & 0 deletions rdtools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
from rdtools.filtering import logic_clip_filter
from rdtools.filtering import xgboost_clip_filter
from rdtools.filtering import normalized_filter
from rdtools.filtering import two_way_window_filter
from rdtools.filtering import insolation_filter
from rdtools.filtering import hampel_filter
from rdtools.filtering import hour_angle_filter
from rdtools.filtering import directional_tukey_filter
# from rdtools.soiling import soiling_srr
# from rdtools.soiling import soiling_cods
# from rdtools.soiling import monthly_soiling_rates
Expand Down
1 change: 1 addition & 0 deletions rdtools/analysis_chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ def _call_clearsky_filter(filter_string):
filter_components["clearsky_filter"] = _call_clearsky_filter(
"clearsky_filter"
)

if "sensor_clearsky_filter" in self.filter_params:
filter_components["sensor_clearsky_filter"] = _call_clearsky_filter(
"sensor_clearsky_filter"
Expand Down
88 changes: 87 additions & 1 deletion rdtools/test/analysis_chains_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from rdtools import TrendAnalysis, normalization
from rdtools import TrendAnalysis, normalization, filtering
from conftest import assert_isinstance, assert_warnings
import pytest
import pvlib
Expand Down Expand Up @@ -78,6 +78,15 @@ def sensor_analysis_exp_power(sensor_parameters):
return rd_analysis


@pytest.fixture
def sensor_analysis_aggregated_no_filter(sensor_parameters):
rd_analysis = TrendAnalysis(**sensor_parameters, power_dc_rated=1.0)
rd_analysis.filter_params = {} # disable all index-based filters
rd_analysis.filter_params_aggregated = {}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
return rd_analysis


def test_interpolation(basic_parameters, degradation_trend):

power = degradation_trend
Expand Down Expand Up @@ -213,6 +222,19 @@ def test_filter_components(sensor_parameters):
assert (poa_filter == rd_analysis.sensor_filter_components["poa_filter"]).all()


def test_filter_components_hour_angle(sensor_parameters, cs_input):
lat = cs_input["pvlib_location"].latitude
lon = cs_input["pvlib_location"].longitude
hour_angle_filter = filtering.hour_angle_filter(sensor_parameters["pv"], lat, lon)
rd_analysis = TrendAnalysis(**sensor_parameters, power_dc_rated=1.0)
rd_analysis.pvlib_location = cs_input['pvlib_location']
rd_analysis.filter_params = {'hour_angle_filter': {}}
rd_analysis.filter_params_aggregated = {}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
assert (hour_angle_filter[1:] ==
rd_analysis.sensor_filter_components["hour_angle_filter"]).all()


def test_aggregated_filter_components(sensor_parameters):
daily_ad_hoc_filter = pd.Series(True, index=sensor_parameters["pv"].index)
daily_ad_hoc_filter[:600] = False
Expand Down Expand Up @@ -247,6 +269,51 @@ def test_aggregated_filter_components_no_filters(sensor_parameters):
assert rd_analysis.sensor_filter_components.empty


def test_aggregated_filter_components_two_way_window_filter(sensor_analysis_aggregated_no_filter):
rd_analysis = sensor_analysis_aggregated_no_filter
aggregated_no_filter = rd_analysis.sensor_aggregated_performance
rd_analysis.filter_params_aggregated = {"two_way_window_filter": {}}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
daily_expected = filtering.two_way_window_filter(aggregated_no_filter)
pd.testing.assert_series_equal(
rd_analysis.sensor_filter_aggregated, daily_expected, check_names=False
)


def test_aggregated_filter_components_insolation_filter(sensor_analysis_aggregated_no_filter):
rd_analysis = sensor_analysis_aggregated_no_filter
aggregated_no_filter = rd_analysis.sensor_aggregated_performance
rd_analysis.filter_params_aggregated = {"insolation_filter": {}}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
daily_expected = filtering.insolation_filter(aggregated_no_filter)
pd.testing.assert_series_equal(
rd_analysis.sensor_filter_aggregated, daily_expected, check_names=False
)


def test_aggregated_filter_components_hampel_filter(sensor_analysis_aggregated_no_filter):
rd_analysis = sensor_analysis_aggregated_no_filter
aggregated_no_filter = rd_analysis.sensor_aggregated_performance
rd_analysis.filter_params_aggregated = {"hampel_filter": {}}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
daily_expected = filtering.hampel_filter(aggregated_no_filter)
pd.testing.assert_series_equal(
rd_analysis.sensor_filter_aggregated, daily_expected, check_names=False
)


def test_aggregated_filter_components_directional_tukey_filter(
sensor_analysis_aggregated_no_filter):
rd_analysis = sensor_analysis_aggregated_no_filter
aggregated_no_filter = rd_analysis.sensor_aggregated_performance
rd_analysis.filter_params_aggregated = {"directional_tukey_filter": {}}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
daily_expected = filtering.directional_tukey_filter(aggregated_no_filter)
pd.testing.assert_series_equal(
rd_analysis.sensor_filter_aggregated, daily_expected, check_names=False
)


@pytest.mark.parametrize("workflow", ["sensor", "clearsky"])
def test_filter_ad_hoc_warnings(workflow, sensor_parameters):
rd_analysis = TrendAnalysis(**sensor_parameters, power_dc_rated=1.0)
Expand Down Expand Up @@ -400,6 +467,16 @@ def clearsky_optional(cs_input, clearsky_analysis):
return extras


@pytest.fixture
def sensor_clearsky_analysis(cs_input, clearsky_parameters):
rd_analysis = TrendAnalysis(**clearsky_parameters)
rd_analysis.set_clearsky(**cs_input)
rd_analysis.filter_params = {} # disable all index-based filters
rd_analysis.filter_params["sensor_clearsky_filter"] = {"model": "csi"}
rd_analysis.sensor_analysis(analyses=["yoy_degradation"])
return rd_analysis


def test_clearsky_analysis(clearsky_analysis):
yoy_results = clearsky_analysis.results["clearsky"]["yoy_degradation"]
ci = yoy_results["rd_confidence_interval"]
Expand All @@ -423,6 +500,15 @@ def test_clearsky_analysis_optional(
assert [-4.71, -4.69] == pytest.approx(ci, abs=1e-2)


def test_sensor_clearsky_analysis(sensor_clearsky_analysis):
yoy_results = sensor_clearsky_analysis.results["sensor"]["yoy_degradation"]
ci = yoy_results["rd_confidence_interval"]
rd = yoy_results["p50_rd"]
print(ci)
assert -5.18 == pytest.approx(rd, abs=1e-2)
assert [-5.18, -5.18] == pytest.approx(ci, abs=1e-2)


@pytest.fixture
def clearsky_analysis_exp_power(clearsky_parameters, clearsky_optional):
power_expected = normalization.pvwatts_dc_power(
Expand Down
78 changes: 52 additions & 26 deletions rdtools/test/bootstrap_test.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,58 @@
'''Bootstrap module tests.'''
"""Bootstrap module tests."""

from rdtools.bootstrap import _construct_confidence_intervals, \
_make_time_series_bootstrap_samples
import pytest

from rdtools.bootstrap import (
_construct_confidence_intervals,
_make_time_series_bootstrap_samples,
)
from rdtools.degradation import degradation_year_on_year


def test_bootstrap_module(cods_normalized_daily, cods_normalized_daily_wo_noise):
''' Test make time serie bootstrap samples and construct of confidence intervals. '''
# Test make bootstrap samples
bootstrap_samples = _make_time_series_bootstrap_samples(cods_normalized_daily,
cods_normalized_daily_wo_noise,
sample_nr=10,
block_length=90,
decomposition_type='multiplicative')
# Check if results are as expected
assert (bootstrap_samples.index == cods_normalized_daily.index).all(), \
"Index of bootstrapped signals is not as expected"
assert bootstrap_samples.shape[1] == 10, "Number of columns in bootstrapped signals is wrong"
@pytest.mark.parametrize("decomposition_type", ["multiplicative", "additive", "error"])
def test_bootstrap_module(
cods_normalized_daily, cods_normalized_daily_wo_noise, decomposition_type
):

if decomposition_type == "error":
with pytest.raises(ValueError):
_make_time_series_bootstrap_samples(
cods_normalized_daily,
cods_normalized_daily_wo_noise,
decomposition_type=decomposition_type)
else:
# Rest make time serie bootstrap samples and construct of confidence intervals.
# Test make bootstrap samples
bootstrap_samples = _make_time_series_bootstrap_samples(
cods_normalized_daily,
cods_normalized_daily_wo_noise,
sample_nr=10,
block_length=90,
decomposition_type=decomposition_type,
)
# Check if results are as expected
assert (
bootstrap_samples.index == cods_normalized_daily.index
).all(), "Index of bootstrapped signals is not as expected"
assert (
bootstrap_samples.shape[1] == 10
), "Number of columns in bootstrapped signals is wrong"

# Test construction of confidence intervals
confidence_intervals, exceedance_level, metrics = _construct_confidence_intervals(
bootstrap_samples, degradation_year_on_year, uncertainty_method='none')
# Test construction of confidence intervals
confidence_intervals, exceedance_level, metrics = (
_construct_confidence_intervals(
bootstrap_samples, degradation_year_on_year, uncertainty_method="none"
)
)

# Check if results are as expected
assert len(confidence_intervals) == 2, "2 confidence interval bounds not returned"
assert isinstance(confidence_intervals[0], float) and \
isinstance(confidence_intervals[1], float), "Confidence interval bounds are not float"
assert isinstance(exceedance_level, float), "Exceedance level is not float"
assert len(metrics) == 10, "Length of metrics is not as expected"
for m in metrics:
assert isinstance(m, float), "Not all metrics are float"
# Check if results are as expected
assert (
len(confidence_intervals) == 2
), "2 confidence interval bounds not returned"
assert isinstance(confidence_intervals[0], float) and isinstance(
confidence_intervals[1], float
), "Confidence interval bounds are not float"
assert isinstance(exceedance_level, float), "Exceedance level is not float"
assert len(metrics) == 10, "Length of metrics is not as expected"
for m in metrics:
assert isinstance(m, float), "Not all metrics are float"
Loading

0 comments on commit 8d67b24

Please sign in to comment.