diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 09c2233ade..43cfd5afe2 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -298,7 +298,7 @@ PVWatts model pvsystem.pvwatts_ac pvsystem.pvwatts_losses -Functions for fitting PV models +Functions for fitting diode models ------------------------------- .. autosummary:: :toctree: generated/ @@ -307,6 +307,14 @@ Functions for fitting PV models ivtools.fit_sdm_cec_sam ivtools.fit_sdm_desoto +Losses +------ +.. autosummary:: + :toctree: generated/ + + losses.soiling_hsu + + Other ----- diff --git a/docs/sphinx/source/whatsnew/v0.7.1.rst b/docs/sphinx/source/whatsnew/v0.7.1.rst index ac88a28f2e..c949c36675 100644 --- a/docs/sphinx/source/whatsnew/v0.7.1.rst +++ b/docs/sphinx/source/whatsnew/v0.7.1.rst @@ -17,6 +17,9 @@ Enhancements * Added :py:func:`~pvlib.iotools.get_pvgis_tmy` to get PVGIS TMY datasets. * Added :py:func:`~pvlib.iotools.parse_epw` to parse a file-like buffer containing weather data in the EPW format. +* Added a new module `pvlib.losses` for various loss models. +* Added the Humboldt State University soiling model + :py:func:`~pvlib.losses.soiling_hsu`. (:issue:`739`) Bug fixes ~~~~~~~~~ @@ -39,3 +42,4 @@ Contributors ~~~~~~~~~~~~ * Kevin Anderson (:ghuser:`kanderso-nrel`) * Mark Mikofski (:ghuser:`mikofski`) +* Valliappan CA (:ghuser:`nappaillav`) diff --git a/pvlib/__init__.py b/pvlib/__init__.py index c99d2e9fd7..098202c819 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -14,3 +14,4 @@ from pvlib import modelchain # noqa: F401 from pvlib import singlediode # noqa: F401 from pvlib import bifacial # noqa: F401 +from pvlib import losses # noqa: F401 diff --git a/pvlib/losses.py b/pvlib/losses.py new file mode 100644 index 0000000000..cd93c0980f --- /dev/null +++ b/pvlib/losses.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +""" +This module contains functions for losses of various types: soiling, mismatch, +snow cover, etc. +""" + +import numpy as np +import pandas as pd +from pvlib.tools import cosd + + +def soiling_hsu(rainfall, cleaning_threshold, tilt, pm2_5, pm10, + depo_veloc={'2_5': 0.004, '10': 0.0009}, + rain_accum_period=pd.Timedelta('1h')): + """ + Calculates soiling ratio given particulate and rain data using the model + from Humboldt State University [1]_. + + Parameters + ---------- + + rainfall : Series + Rain accumulated in each time period. [mm] + + cleaning_threshold : float + Amount of rain in an accumulation period needed to clean the PV + modules. [mm] + + tilt : float + Tilt of the PV panels from horizontal. [degree] + + pm2_5 : numeric + Concentration of airborne particulate matter (PM) with + aerodynamic diameter less than 2.5 microns. [g/m^3] + + pm10 : numeric + Concentration of airborne particulate matter (PM) with + aerodynamicdiameter less than 10 microns. [g/m^3] + + depo_veloc : dict, default {'2_5': 0.4, '10': 0.09} + Deposition or settling velocity of particulates. [m/s] + + rain_accum_period : Timedelta, default 1 hour + Period for accumulating rainfall to check against `cleaning_threshold` + It is recommended that `rain_accum_period` be between 1 hour and + 24 hours. + + Returns + ------- + soiling_ratio : Series + Values between 0 and 1. Equal to 1 - transmission loss. + + References + ----------- + .. [1] M. Coello and L. Boyle, "Simple Model For Predicting Time Series + Soiling of Photovoltaic Panels," in IEEE Journal of Photovoltaics. + doi: 10.1109/JPHOTOV.2019.2919628 + .. [2] Atmospheric Chemistry and Physics: From Air Pollution to Climate + Change. J. Seinfeld and S. Pandis. Wiley and Sons 2001. + + """ + try: + from scipy.special import erf + except ImportError: + raise ImportError("The soiling_hsu function requires scipy.") + + # accumulate rainfall into periods for comparison with threshold + accum_rain = rainfall.rolling(rain_accum_period, closed='right').sum() + # cleaning is True for intervals with rainfall greater than threshold + cleaning_times = accum_rain.index[accum_rain >= cleaning_threshold] + + horiz_mass_rate = pm2_5 * depo_veloc['2_5']\ + + np.maximum(pm10 - pm2_5, 0.) * depo_veloc['10'] + tilted_mass_rate = horiz_mass_rate * cosd(tilt) # assuming no rain + + # tms -> tilt_mass_rate + tms_cumsum = np.cumsum(tilted_mass_rate * np.ones(rainfall.shape)) + + mass_no_cleaning = pd.Series(index=rainfall.index, data=tms_cumsum) + mass_removed = pd.Series(index=rainfall.index) + mass_removed[0] = 0. + mass_removed[cleaning_times] = mass_no_cleaning[cleaning_times] + accum_mass = mass_no_cleaning - mass_removed.ffill() + + soiling_ratio = 1 - 0.3437 * erf(0.17 * accum_mass**0.8473) + + return soiling_ratio diff --git a/pvlib/tests/test_losses.py b/pvlib/tests/test_losses.py new file mode 100644 index 0000000000..c6081c7cfd --- /dev/null +++ b/pvlib/tests/test_losses.py @@ -0,0 +1,92 @@ +import pandas as pd +from pandas.util.testing import assert_series_equal +from pvlib.losses import soiling_hsu +from conftest import requires_scipy +import pytest + + +@pytest.fixture +def expected_output(): + # Sample output (calculated manually) + dt = pd.date_range(start=pd.datetime(2019, 1, 1, 0, 0, 0), + end=pd.datetime(2019, 1, 1, 23, 59, 0), freq='1h') + + expected_no_cleaning = pd.Series( + data=[0.884980357535360, 0.806308930084762, 0.749974647038078, + 0.711804155175089, 0.687489866078621, 0.672927554408964, + 0.664714899337491, 0.660345851212099, 0.658149551658860, + 0.657104593968981, 0.656633344364056, 0.656431630729954, + 0.656349579062171, 0.656317825078228, 0.656306121502393, + 0.656302009396500, 0.656300630853678, 0.656300189543417, + 0.656300054532516, 0.656300015031680, 0.656300003971846, + 0.656300001006533, 0.656300000244750, 0.656300000057132], + index=dt) + + return expected_no_cleaning + + +@pytest.fixture +def expected_output_2(expected_output): + # Sample output (calculated manually) + dt = pd.date_range(start=pd.datetime(2019, 1, 1, 0, 0, 0), + end=pd.datetime(2019, 1, 1, 23, 59, 0), freq='1h') + + expected_no_cleaning = expected_output + + expected = pd.Series(index=dt) + expected[dt[:4]] = expected_no_cleaning[dt[:4]] + expected[dt[4:7]] = 1. + expected[dt[7]] = expected_no_cleaning[dt[0]] + expected[dt[8:12]] = 1. + expected[dt[12:17]] = expected_no_cleaning[dt[:5]] + expected[dt[17:21]] = 1. + expected[dt[21:]] = expected_no_cleaning[:3] + + return expected + + +@pytest.fixture +def rainfall_input(): + + dt = pd.date_range(start=pd.datetime(2019, 1, 1, 0, 0, 0), + end=pd.datetime(2019, 1, 1, 23, 59, 0), freq='1h') + rainfall = pd.Series( + data=[0., 0., 0., 0., 1., 0., 0., 0., 0.5, 0.5, 0., 0., 0., 0., 0., + 0., 0.3, 0.3, 0.3, 0.3, 0., 0., 0., 0.], index=dt) + return rainfall + + +@requires_scipy +def test_soiling_hsu_no_cleaning(rainfall_input, expected_output): + """Test Soiling HSU function""" + + rainfall = rainfall_input + pm2_5 = 1.0 + pm10 = 2.0 + depo_veloc = {'2_5': 1.0, '10': 1.0} + tilt = 0. + expected_no_cleaning = expected_output + + result = soiling_hsu(rainfall=rainfall, cleaning_threshold=10., tilt=tilt, + pm2_5=pm2_5, pm10=pm10, depo_veloc=depo_veloc, + rain_accum_period=pd.Timedelta('1h')) + assert_series_equal(result, expected_no_cleaning) + + +@requires_scipy +def test_soiling_hsu(rainfall_input, expected_output_2): + """Test Soiling HSU function""" + + rainfall = rainfall_input + pm2_5 = 1.0 + pm10 = 2.0 + depo_veloc = {'2_5': 1.0, '10': 1.0} + tilt = 0. + expected = expected_output_2 + + # three cleaning events at 4:00-6:00, 8:00-11:00, and 17:00-20:00 + result = soiling_hsu(rainfall=rainfall, cleaning_threshold=0.5, tilt=tilt, + pm2_5=pm2_5, pm10=pm10, depo_veloc=depo_veloc, + rain_accum_period=pd.Timedelta('3h')) + + assert_series_equal(result, expected)