Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/monthly soiling metrics #193

Merged
merged 34 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6120b2a
Add monthly soiling calculations
mdeceglie Aug 20, 2020
f42b432
Add the monthly calculations to example notebook
mdeceglie Aug 20, 2020
41bf89d
update api.rst
mdeceglie Aug 20, 2020
d774086
update changelog
mdeceglie Aug 20, 2020
516cd81
clean up docs
mdeceglie Aug 20, 2020
ec41fb3
Add annual soiling ratio calculation
mdeceglie Aug 31, 2020
509c19b
Add parameter to control confidence interval in annual and monthly fu…
mdeceglie Aug 31, 2020
580bb02
update sphinx
mdeceglie Aug 31, 2020
4a5afb5
minor pep8 clean up
mdeceglie Sep 1, 2020
5a5f0d1
annual_soiling_ratios tests
mdeceglie Sep 1, 2020
4a9bfa7
Update pandas in requirements
mdeceglie Sep 1, 2020
e6b591c
Fix bug that occurs when some months are untouched by all intervals
mdeceglie Sep 1, 2020
fbdc75e
further bug fix for untouched months
mdeceglie Sep 2, 2020
2a718f6
monthly_soiling_rates tests
mdeceglie Sep 2, 2020
2cd3017
update notebook kernel
mdeceglie Sep 3, 2020
bc07fcc
Format docstring for sphinx
mdeceglie Sep 24, 2020
0ae9390
typo fix
mdeceglie Sep 24, 2020
81ded1a
Doc updates
mdeceglie Sep 24, 2020
abeaaff
Change output of annual_soiling_ratios()
mdeceglie Sep 24, 2020
65e677f
bump matplotlib version
mdeceglie Sep 24, 2020
2b6c354
Run notebook
mdeceglie Sep 24, 2020
a8ed8a7
docs typo
mdeceglie Sep 25, 2020
8fec43c
simplify annual_soiling_ratios()
mdeceglie Sep 29, 2020
f043df3
Correct monthly_soiling_rates() docstring
mdeceglie Sep 29, 2020
1c60ad5
Reword `monthly_soiling_rates()` docstring
mdeceglie Sep 29, 2020
b3f2643
rename min_interval_length
mdeceglie Sep 29, 2020
cb37c32
Update soiilng docstrings
mdeceglie Sep 30, 2020
7a7bef8
Change from slope to soiling rate
mdeceglie Sep 30, 2020
187dac4
typo
mdeceglie Sep 30, 2020
e0aaa7d
Fix table
mdeceglie Sep 30, 2020
800b821
add soiling rate units
mdeceglie Sep 30, 2020
27cb676
turn off smartquotes
mdeceglie Sep 30, 2020
3d40124
Changlog entry for soiling interval summary column name changes
mdeceglie Oct 1, 2020
7ba50c3
Merge branch 'development' into feat/monthly_soiling_metrics
mdeceglie Oct 1, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 360 additions & 33 deletions docs/degradation_and_soiling_example_pvdaq_4.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Functions for estimating soiling rates from PV system data.
:toctree: generated/

soiling.soiling_srr
soiling.monthly_soiling_rates
soiling.annual_soiling_ratios
soiling.SRRAnalysis
soiling.SRRAnalysis.run

Expand Down
10 changes: 8 additions & 2 deletions docs/sphinx/source/changelog/pending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ Pending
API Changes
-----------

* Change the column names 'slope', 'slope_low', and 'slope_high' to 'soiling_rate', 'soiling_rate_low', and 'soiling_rate_high' in ``calc_info['soiling_interval_summary']`` returned from :py:meth:`~rdtools.soiling.SRRAnalysis.run()` and :py:func:`rdtools.soiling.soiling_srr()` (:pull:`193`).


Enhancements
------------

* Add new function :py:func:`~rdtools.soiling.monthly_soiling_rates` (:pull:`193`).
* Add new function :py:func:`~rdtools.annual_soiling_ratios` (:pull:`193`).
* Create new module :py:mod:`~rdtools.availability` and class
:py:class:`~rdtools.availability.AvailabilityAanlysis` for estimating
timeseries system availability (:pull:`131`)
Expand All @@ -27,16 +32,17 @@ Documentation
* Update landing page and add new "Inverter Downtime" documentation page
based on the availability notebook (:pull:`131`)


Requirements
------------


Example Updates
---------------
* :py:func:`~rdtools.soiling.monthly_soiling_rates` added to degradation_and_soiling_example_pvdaq_4.ipynb
mdeceglie marked this conversation as resolved.
Show resolved Hide resolved
* Add new ``system_availability_example.ipynb`` notebook (:pull:`131`)


Contributors
------------
* Kevin Anderson (:ghuser:`kanderso-nrel`)
* Mike Deceglie (:ghuser:`mdeceglie`)
1 change: 1 addition & 0 deletions docs/sphinx/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static', '_images']
smartquotes = False
Copy link
Member

@kandersolar kandersolar Sep 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference, this is because parameter description list entries starting with a quoted string don't render the open-quote character correctly when smartquotes is enabled, see sphinx-doc/sphinx#4028


master_doc = 'index'
# A workaround for the responsive tables always having annoying scrollbars.
Expand Down
2 changes: 2 additions & 0 deletions rdtools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from rdtools.filtering import clip_filter
from rdtools.filtering import normalized_filter
from rdtools.soiling import soiling_srr
from rdtools.soiling import monthly_soiling_rates
from rdtools.soiling import annual_soiling_ratios
from rdtools.plotting import degradation_summary_plots
from rdtools.plotting import soiling_monte_carlo_plot
from rdtools.plotting import soiling_interval_plot
Expand Down
2 changes: 1 addition & 1 deletion rdtools/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def soiling_rate_histogram(soiling_info, bins=None):

soiling_summary = soiling_info['soiling_interval_summary']
fig, ax = plt.subplots()
ax.hist(100.0 * soiling_summary.loc[soiling_summary['valid'], 'slope'],
ax.hist(100.0 * soiling_summary.loc[soiling_summary['valid'], 'soiling_rate'],
bins=bins)
ax.set_xlabel('Soiling rate (%/day)')
ax.set_ylabel('Count')
Expand Down
329 changes: 291 additions & 38 deletions rdtools/soiling.py

Large diffs are not rendered by default.

163 changes: 157 additions & 6 deletions rdtools/test/soiling_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pandas as pd
import numpy as np
from rdtools import soiling_srr
from rdtools import annual_soiling_ratios
from rdtools import monthly_soiling_rates
from rdtools.soiling import NoValidIntervalError
import pytest

Expand Down Expand Up @@ -57,7 +59,7 @@ def test_soiling_srr(normalized_daily, insolation, times):
'soiling_info["stochastic_soiling_profiles"] is not a list'

# Check soiling_info['soiling_interval_summary']
expected_summary_columns = ['start', 'end', 'slope', 'slope_low', 'slope_high',
expected_summary_columns = ['start', 'end', 'soiling_rate', 'soiling_rate_low', 'soiling_rate_high',
'inferred_start_loss', 'inferred_end_loss', 'length', 'valid']
actual_summary_columns = soiling_info['soiling_interval_summary'].columns.values

Expand All @@ -69,14 +71,14 @@ def test_soiling_srr(normalized_daily, insolation, times):
"'{}' was expected as a column, but not in soiling_info['soiling_interval_summary']".format(x)
assert isinstance(soiling_info['soiling_interval_summary'], pd.DataFrame),\
'soiling_info["soiling_interval_summary"] not a dataframe'
expected_means = pd.Series({'slope': -0.002617290,
'slope_low': -0.002828525,
'slope_high': -0.002396639,
expected_means = pd.Series({'soiling_rate': -0.002617290,
'soiling_rate_low': -0.002828525,
'soiling_rate_high': -0.002396639,
'inferred_start_loss': 1.021514,
'inferred_end_loss': 0.9572880,
'length': 24.0,
'valid': 1.0})
expected_means = expected_means[['slope', 'slope_low', 'slope_high',
expected_means = expected_means[['soiling_rate', 'soiling_rate_low', 'soiling_rate_high',
'inferred_start_loss', 'inferred_end_loss',
'length', 'valid']]
pd.testing.assert_series_equal(expected_means, soiling_info['soiling_interval_summary'].mean(),
Expand All @@ -103,7 +105,7 @@ def test_soiling_srr_with_precip(normalized_daily, insolation, times):
sr, sr_ci, soiling_info = soiling_srr(normalized_daily, insolation, clean_criterion='precip_and_shift', **kwargs)
assert 0.983270 == pytest.approx(sr, abs=1e-6),\
"Soiling ratio with clean_criterion='precip_and_shift' different from expected"
np.random.seed(1977)
np.random.seed(1977)
sr, sr_ci, soiling_info = soiling_srr(normalized_daily, insolation, clean_criterion='precip_or_shift', **kwargs)
assert 0.973228 == pytest.approx(sr, abs=1e-6),\
"Soiling ratio with clean_criterion='precip_or_shift' different from expected"
Expand Down Expand Up @@ -212,6 +214,7 @@ def test_soiling_srr_max_negative_slope_error(normalized_daily, insolation):
assert 0.952995 == pytest.approx(sr, abs=1e-6),\
'Soiling ratio different from expected when max_relative_slope_error=50.0'


def test_soiling_srr_with_nan_interval(normalized_daily, insolation, times):
'''
Previous versions had a bug which would have raised an error when an entire interval
Expand All @@ -224,3 +227,151 @@ def test_soiling_srr_with_nan_interval(normalized_daily, insolation, times):
sr, sr_ci, soiling_info = soiling_srr(normalized_corrupt, insolation, reps=reps)
assert 0.947416 == pytest.approx(sr, abs=1e-6),\
'Soiling ratio different from expected value when an entire interval was NaN'

# ###########################
# annual_soiling_ratios tests
# ###########################


@pytest.fixture()
def srr_profiles():
times = pd.date_range('01-01-2018', '12-31-2019', freq='D')
data = np.array([0]*365 + [10]*365)
profiles = [pd.Series(x + data, times) for x in range(10)]

return profiles


def test_annual_soiling_ratios(srr_profiles):
expected_data = np.array([[2018, 4.5, 1.0, 8.0],
[2019, 14.5, 11.0, 18.0]])
expected = pd.DataFrame(data=expected_data,
columns=['year', 'soiling_ratio_median', 'soiling_ratio_low', 'soiling_ratio_high'])
expected['year'] = expected['year'].astype(int)

result = annual_soiling_ratios(srr_profiles)

pd.testing.assert_frame_equal(result, expected, atol=1e-8)


def test_annual_soiling_ratios_confidence_interval(srr_profiles):
expected_data = np.array([[2018, 4.5, 0, 9.0],
[2019, 14.5, 10.0, 19.0]])
expected = pd.DataFrame(data=expected_data,
columns=['year', 'soiling_ratio_median', 'soiling_ratio_low', 'soiling_ratio_high'])
expected['year'] = expected['year'].astype(int)

result = annual_soiling_ratios(srr_profiles, confidence_level=95)

pd.testing.assert_frame_equal(result, expected, atol=1e-8)

# ###########################
# monthly_soiling_rates tests
# ###########################


@pytest.fixture()
def soiling_interval_summary():
starts = ['2019/01/01', '2019/01/16', '2019/02/08', '2019/03/06']
starts = pd.to_datetime(starts).tz_localize('America/Denver')
ends = ['2019/01/15', '2019/02/07', '2019/03/05', '2019/04/07']
ends = pd.to_datetime(ends).tz_localize('America/Denver')
slopes = [-0.005, -0.002, -0.001, -0.002]
slopes_low = [-0.0055, -0.0025, -0.0015, -0.003]
slopes_high = [-0.004, 0, 0, -0.001]
valids = [True, True, False, True]

soiling_interval_summary = pd.DataFrame()
soiling_interval_summary['start'] = starts
soiling_interval_summary['end'] = ends
soiling_interval_summary['soiling_rate'] = slopes
soiling_interval_summary['soiling_rate_low'] = slopes_low
soiling_interval_summary['soiling_rate_high'] = slopes_high
soiling_interval_summary['inferred_start_loss'] = np.nan
soiling_interval_summary['inferred_end_loss'] = np.nan
soiling_interval_summary['length'] = (ends - starts).days
soiling_interval_summary['valid'] = valids

return soiling_interval_summary


def _build_monthly_summary(top_rows):
'''
Convienience function to build a full monthly soiling summary
dataframe from the expected_top_rows which summarize Jan-April
'''

all_rows = np.vstack((top_rows, [[1, np.nan, np.nan, np.nan, 0]]*8))

df = pd.DataFrame(data=all_rows,
columns=['month', 'soiling_rate_median', 'soiling_rate_low', 'soiling_rate_high', 'interval_count'])
df['month'] = range(1, 13)

return df


def test_monthly_soiling_rates(soiling_interval_summary):
np.random.seed(1977)
result = monthly_soiling_rates(soiling_interval_summary)

expected = np.array([[1.00000000e+00, -2.42103810e-03, -5.00912766e-03, -7.68551806e-04, 2.00000000e+00],
[2.00000000e+00, -1.25092837e-03, -2.10091842e-03, -3.97354321e-04, 1.00000000e+00],
[3.00000000e+00, -2.00313359e-03, -2.68359541e-03, -1.31927678e-03, 1.00000000e+00],
[4.00000000e+00, -1.99729563e-03, -2.68067699e-03, -1.31667446e-03, 1.00000000e+00]])
expected = _build_monthly_summary(expected)

pd.testing.assert_frame_equal(result, expected, check_dtype=False)


def test_monthly_soiling_rates_min_interval_length(soiling_interval_summary):
np.random.seed(1977)
result = monthly_soiling_rates(soiling_interval_summary, min_interval_length=20)

expected = np.array([[1.00000000e+00, -1.24851539e-03, -2.10394564e-03, -3.98358211e-04, 1.00000000e+00],
[2.00000000e+00, -1.25092837e-03, -2.10091842e-03, -3.97330424e-04, 1.00000000e+00],
[3.00000000e+00, -2.00309454e-03, -2.68359541e-03, -1.31927678e-03, 1.00000000e+00],
mdeceglie marked this conversation as resolved.
Show resolved Hide resolved
[4.00000000e+00, -1.99729563e-03, -2.68067699e-03, -1.31667446e-03, 1.00000000e+00]])
expected = _build_monthly_summary(expected)

pd.testing.assert_frame_equal(result, expected, check_dtype=False)


def test_monthly_soiling_rates_max_slope_err(soiling_interval_summary):
np.random.seed(1977)
result = monthly_soiling_rates(soiling_interval_summary, max_relative_slope_error=120)

expected = np.array([[1.00000000e+00, -4.74910923e-03, -5.26236739e-03, -4.23901493e-03, 1.00000000e+00],
[2.00000000e+00, np.nan, np.nan, np.nan, 0.00000000e+00],
[3.00000000e+00, -2.00074270e-03, -2.68073474e-03, -1.31786434e-03, 1.00000000e+00],
[4.00000000e+00, -2.00309454e-03, -2.68359541e-03, -1.31927678e-03, 1.00000000e+00]])
expected = _build_monthly_summary(expected)

pd.testing.assert_frame_equal(result, expected, check_dtype=False)


def test_monthly_soiling_rates_confidence_level(soiling_interval_summary):
np.random.seed(1977)
result = monthly_soiling_rates(soiling_interval_summary, confidence_level=95)

expected = np.array([[1.00000000e+00, -2.42103810e-03, -5.42313113e-03, -1.21156562e-04, 2.00000000e+00],
[2.00000000e+00, -1.25092837e-03, -2.43731574e-03, -6.23842627e-05, 1.00000000e+00],
[3.00000000e+00, -2.00313359e-03, -2.94998476e-03, -1.04988760e-03, 1.00000000e+00],
[4.00000000e+00, -1.99729563e-03, -2.95063841e-03, -1.04869949e-03, 1.00000000e+00]])

expected = _build_monthly_summary(expected)

pd.testing.assert_frame_equal(result, expected, check_dtype=False)


def test_monthly_soiling_rates_reps(soiling_interval_summary):
np.random.seed(1977)
result = monthly_soiling_rates(soiling_interval_summary, reps=3)

expected = np.array([[1.00000000e+00, -2.88594088e-03, -5.03736679e-03, -6.47391131e-04, 2.00000000e+00],
[2.00000000e+00, -1.67359565e-03, -2.00504171e-03, -1.33240044e-03, 1.00000000e+00],
[3.00000000e+00, -1.22306993e-03, -2.19274892e-03, -1.11793240e-03, 1.00000000e+00],
[4.00000000e+00, -1.94675549e-03, -2.42574164e-03, -1.54850795e-03, 1.00000000e+00]])

expected = _build_monthly_summary(expected)

pd.testing.assert_frame_equal(result, expected, check_dtype=False)
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
cycler==0.10.0
h5py==2.10.0
kiwisolver==1.2.0
matplotlib==3.1.2
matplotlib==3.3.2
mdeceglie marked this conversation as resolved.
Show resolved Hide resolved
nbsphinx==0.4.3
nbsphinx-link==1.3.0
numpy==1.17.3
pandas==1.0.3
pandas==1.1.0
patsy==0.5.1
pvlib==0.7.1
pyparsing==2.4.7
Expand Down