Skip to content

Commit

Permalink
return results instead of none when CODS soiling signal is small (#400)
Browse files Browse the repository at this point in the history
* return results instead of none

* change dep. less_precise to rtol in assert

* flake8 fix missing whitespace

* flake8 fix missing whitespace

* specify dtype seasonal_samples

* Re run notebook

* add CODS small signal test

* update arch version

* update packages re-run notebook

* update change log

* Add arch bump to the changelog

---------

Co-authored-by: Michael Deceglie <[email protected]>
Co-authored-by: Michael Deceglie <[email protected]>
  • Loading branch information
3 people authored Dec 1, 2023
1 parent 147962b commit 250e412
Show file tree
Hide file tree
Showing 9 changed files with 4,111 additions and 4,153 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ rdtools.egg-info*
.\#*

*.pickle

# ignore vscode settings
.vscode/
8,153 changes: 4,030 additions & 4,123 deletions docs/TrendAnalysis_example_pvdaq4.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
RdTools Change Log
==================
.. include:: changelog/v2.2.0-beta.2.rst
.. include:: changelog/v2.2.0-beta.1.rst
.. include:: changelog/v2.1.8.rst
.. include:: changelog/v2.1.7.rst
Expand Down
23 changes: 23 additions & 0 deletions docs/sphinx/source/changelog/v2.2.0-beta.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
********************************
v2.2.0-beta.2 (December 1, 2023)
********************************

Enhancements
------------
* Return CODS results without bootstrapping when soiling signal
is small but raise warning ``soiling.py`` (:issue:`367` :pull:`400`)

Bug fixes
---------
* Fix flake8 missing whitespaces ``bootstrap_test.py``, ``soiling_cods_test.py`` (:pull:`400`)
* Specify dtype for seasonal samples ``soiling.py`` (:pull:`400`)
* Update deprecated `check_less_precise` to `rtol` ``soiling_cods_test.py`` (:pull:`400`)

Requirements
------------
* Bump arch to 5.6.0 in ``requirements.txt``

Contributors
------------
* Martin Springer (:ghuser:`martin-springer`)
* Michael Deceglie (:ghuser:`mdeceglie`)
13 changes: 7 additions & 6 deletions rdtools/soiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -1762,11 +1762,11 @@ def run_bootstrap(self,
self.soiling_loss = [0, 0, (1 - result_df.soiling_ratio).mean()]
self.small_soiling_signal = True
self.errors = (
'Soiling signal is small relative to the noise.'
'Iterative decomposition not possible.\n'
'Degradation found by RdTools YoY')
print(self.errors)
return
'Soiling signal is small relative to the noise. '
'Iterative decomposition not possible. '
'Degradation found by RdTools YoY.')
warnings.warn(self.errors)
return self.result_df, self.degradation, self.soiling_loss
self.small_soiling_signal = False

# Aggregate all bootstrap samples
Expand Down Expand Up @@ -2507,7 +2507,8 @@ def _make_seasonal_samples(list_of_SCs, sample_nr=10, min_multiplier=0.5,
''' Generate seasonal samples by perturbing the amplitude and the phase of
a seasonal components found with the fitted CODS model '''
samples = pd.DataFrame(index=list_of_SCs[0].index,
columns=range(int(sample_nr*len(list_of_SCs))))
columns=range(int(sample_nr*len(list_of_SCs))),
dtype=float)
# From each fitted signal, we will generate new seaonal components
for i, signal in enumerate(list_of_SCs):
# Remove beginning and end of signal
Expand Down
2 changes: 1 addition & 1 deletion rdtools/test/bootstrap_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'''Bootstrap module tests.'''

from rdtools.bootstrap import _construct_confidence_intervals,\
from rdtools.bootstrap import _construct_confidence_intervals, \
_make_time_series_bootstrap_samples
from rdtools.degradation import degradation_year_on_year

Expand Down
10 changes: 10 additions & 0 deletions rdtools/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ def cods_normalized_daily(cods_normalized_daily_wo_noise):
return cods_normalized_daily


@pytest.fixture()
def cods_normalized_daily_small_soiling(cods_normalized_daily_wo_noise):
N = len(cods_normalized_daily_wo_noise)
np.random.seed(1977)
noise = 1 + 0.02 * (np.random.rand(N) - 0.5)
cods_normalized_daily_small_soiling = cods_normalized_daily_wo_noise.apply(
lambda row: 1-(1-row)*0.1) * noise
return cods_normalized_daily_small_soiling


# %% Availability fixtures

ENERGY_PARAMETER_SPACE = list(itertools.product(
Expand Down
57 changes: 35 additions & 22 deletions rdtools/test/soiling_cods_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ def test_iterative_signal_decomposition(cods_normalized_daily):
cods = soiling.CODSAnalysis(cods_normalized_daily)
df_out, results_dict = \
cods.iterative_signal_decomposition()
assert 0.080641 == pytest.approx(results_dict['degradation'], abs=1e-6),\
assert 0.080641 == pytest.approx(results_dict['degradation'], abs=1e-6), \
'Degradation rate different from expected value'
assert 3.305136 == pytest.approx(results_dict['soiling_loss'], abs=1e-6),\
assert 3.305136 == pytest.approx(results_dict['soiling_loss'], abs=1e-6), \
'Soiling loss different from expected value'
assert 0.999359 == pytest.approx(results_dict['residual_shift'], abs=1e-6),\
assert 0.999359 == pytest.approx(results_dict['residual_shift'], abs=1e-6), \
'Residual shift different from expected value'
assert 0.008144 == pytest.approx(results_dict['RMSE'], abs=1e-6),\
assert 0.008144 == pytest.approx(results_dict['RMSE'], abs=1e-6), \
'RMSE different from expected value'
assert not results_dict['small_soiling_signal'], \
'Small soiling signal assertion different from expected value'
assert 7.019626e-11 == pytest.approx(results_dict['adf_res'][1], abs=1e-6),\
assert 7.019626e-11 == pytest.approx(results_dict['adf_res'][1], abs=1e-6), \
'p-value of Augmented Dickey-Fuller test different from expected value'

# Check result dataframe
Expand All @@ -31,10 +31,10 @@ def test_iterative_signal_decomposition(cods_normalized_daily):
'seasonal_component', 'degradation_trend', 'total_model', 'residuals']
actual_columns = df_out.columns.values
for x in actual_columns:
assert x in expected_columns,\
assert x in expected_columns, \
"'{}' not an expected column in result_df]".format(x)
for x in expected_columns:
assert x in actual_columns,\
assert x in actual_columns, \
"'{}' was expected as a column, but not in result_df".format(x)
assert isinstance(df_out, pd.DataFrame), 'result_df not a dataframe'
expected_means = pd.Series({'soiling_ratio': 0.9669486267086722,
Expand All @@ -48,7 +48,7 @@ def test_iterative_signal_decomposition(cods_normalized_daily):
['soiling_ratio', 'soiling_rates', 'cleaning_events',
'seasonal_component', 'degradation_trend', 'total_model', 'residuals']]
pd.testing.assert_series_equal(expected_means, df_out.mean(),
check_exact=False, check_less_precise=True)
check_exact=False, rtol=1e-3)


def test_iterative_signal_decomposition_with_nan_interval(cods_normalized_daily):
Expand All @@ -59,17 +59,17 @@ def test_iterative_signal_decomposition_with_nan_interval(cods_normalized_daily)
cods = soiling.CODSAnalysis(normalized_corrupt)
df_out, results_dict = \
cods.iterative_signal_decomposition()
assert -0.004968 == pytest.approx(results_dict['degradation'], abs=1e-5),\
assert -0.004968 == pytest.approx(results_dict['degradation'], abs=1e-5), \
'Degradation rate different from expected value'
assert 3.232171 == pytest.approx(results_dict['soiling_loss'], abs=1e-5),\
assert 3.232171 == pytest.approx(results_dict['soiling_loss'], abs=1e-5), \
'Soiling loss different from expected value'
assert 1.000108 == pytest.approx(results_dict['residual_shift'], abs=1e-5),\
assert 1.000108 == pytest.approx(results_dict['residual_shift'], abs=1e-5), \
'Residual shift different from expected value'
assert 0.008184 == pytest.approx(results_dict['RMSE'], abs=1e-5),\
assert 0.008184 == pytest.approx(results_dict['RMSE'], abs=1e-5), \
'RMSE different from expected value'
assert not results_dict['small_soiling_signal'], \
'Small soiling signal assertion different from expected value'
assert 1.230754e-8 == pytest.approx(results_dict['adf_res'][1], abs=1e-6),\
assert 1.230754e-8 == pytest.approx(results_dict['adf_res'][1], abs=1e-6), \
'p-value of Augmented Dickey-Fuller test different from expected value'

# Check result dataframe
Expand All @@ -85,21 +85,21 @@ def test_iterative_signal_decomposition_with_nan_interval(cods_normalized_daily)
['soiling_ratio', 'soiling_rates', 'cleaning_events',
'seasonal_component', 'degradation_trend', 'total_model', 'residuals']]
pd.testing.assert_series_equal(expected_means, df_out.mean(),
check_exact=False, check_less_precise=True)
check_exact=False, rtol=1e-3)


def test_soiling_cods(cods_normalized_daily):
''' Test the CODS algorithm with fixed test case and 16 repetitions'''
reps = 16
np.random.seed(1977)
sr, sr_ci, deg, deg_ci, result_df = soiling.soiling_cods(cods_normalized_daily, reps=reps)
assert 0.962207 == pytest.approx(sr, abs=0.5),\
assert 0.962207 == pytest.approx(sr, abs=0.5), \
'Soiling ratio different from expected value'
assert np.array([0.96662419, 0.95692131]) == pytest.approx(sr_ci, abs=0.5),\
assert np.array([0.96662419, 0.95692131]) == pytest.approx(sr_ci, abs=0.5), \
'Confidence interval of SR different from expected value'
assert 0.09 == pytest.approx(deg, abs=0.5),\
assert 0.09 == pytest.approx(deg, abs=0.5), \
'Degradation rate different from expected value'
assert np.array([-0.17143952, 0.39313724]) == pytest.approx(deg_ci, abs=0.5),\
assert np.array([-0.17143952, 0.39313724]) == pytest.approx(deg_ci, abs=0.5), \
'Confidence interval of degradation rate different from expected value'

# Check result dataframe
Expand All @@ -111,13 +111,26 @@ def test_soiling_cods(cods_normalized_daily):
'model_high']
actual_summary_columns = result_df.columns.values
for x in actual_summary_columns:
assert x in expected_summary_columns,\
assert x in expected_summary_columns, \
"'{}' not an expected column in result_df]".format(x)
for x in expected_summary_columns:
assert x in actual_summary_columns,\
assert x in actual_summary_columns, \
"'{}' was expected as a column, but not in result_df".format(x)


def test_soiling_cods_small_signal(cods_normalized_daily_small_soiling):
''' Test the CODS algorithm with small soiling signal'''
reps = 16
np.random.seed(1977)
warn_small_signal = (
'Soiling signal is small relative to the noise. '
'Iterative decomposition not possible. '
'Degradation found by RdTools YoY.')

with pytest.warns(UserWarning, match=warn_small_signal):
soiling.soiling_cods(cods_normalized_daily_small_soiling, reps=reps)


def test_Kalman_filter_for_SR(cods_normalized_daily):
'''Test the Kalman Filter method in CODS'''
cods = soiling.CODSAnalysis(cods_normalized_daily)
Expand All @@ -131,10 +144,10 @@ def test_Kalman_filter_for_SR(cods_normalized_daily):
'soiling_rates', 'cleaning_events', 'days_since_ce']
actual_columns = dfk.columns.values
for x in actual_columns:
assert x in expected_columns,\
assert x in expected_columns, \
"'{}' not an expected column in Kalman Filter results]".format(x)
for x in expected_columns:
assert x in actual_columns,\
assert x in actual_columns, \
"'{}' was expected as a column, but not in Kalman Filter results".format(x)
assert Ps.shape == (732, 2, 2), "Shape of array of covariance matrices (Ps) not as expected"

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pvlib==0.9.0
pyparsing==2.4.7
python-dateutil==2.8.1
pytz==2019.3
arch==4.11
arch==5.6.0
filterpy==1.4.5
requests==2.31.0
retrying==1.3.3
Expand Down

0 comments on commit 250e412

Please sign in to comment.