pvlib / pvlib-python

A set of documented functions for simulating the performance of photovoltaic energy systems.
https://pvlib-python.readthedocs.io
BSD 3-Clause "New" or "Revised" License
1.16k stars 979 forks source link

Kimber Dynamic Soiling model #669

Closed mikofski closed 4 years ago

mikofski commented 5 years ago

related to #61 and #668.

Kimber dynamic soiling model has 3 parameters:

tahentx commented 5 years ago

@mikofski @cwhanse regarding soiling rate, the authors arrived at a range of .1% - .3% a day, using a sample of 46 projects. Given that, I'm wondering if there is a better way than simply splitting the difference and assuming a static value of .2% in the model. The random and localized nature of this loss factor in particular presents a challenge. It seems we can either assume static values for the 3 parameters based on the averages provided by Kimber, or derive them from historical weather data (which might not always be available). Thoughts? I'd like to help write this method, but want to agree on a defensible approach.

mikofski commented 5 years ago

Hi @tahentx, My preference would be to leave the Kimber model as is, and let users decide for themselves. IMO pvlib should not develop new research, but present the current state of the art as understood and published in the literature. 😀

But there are other soiling models, so maybe we should provide a few others as well:

  1. NREL had recommended a constant rate of like between 0.03%/day and 0.25%/day 2017 time series anaysis and seasonal soiling rates

  2. @zdefreitas created an update to the Kimber model that uses the Köppen-Geiger climate classification. This is the model currently used in sunpower pvsim, but afaik it's never been published

  3. Zoe, Sarah Kurtz, and @mdeceglie have a new paper this year on a new method soiling rates

mikofski commented 5 years ago

@tahentx, here's an idea, could you make these parameters arguments?

def soiling_kimber(rainfall_timeseries, soiling_rate_daily=0.0015, grace_period=14,
                   rain_threshold=30, locale='rural', scheduled_cleaning_dates=None):
"""
Kimber model reference. If soiling rate is None, and locale is set,
then defaults are used. See ref.
"""
    return soiling

This way user could override default.

BTW: Thanks for taking this on!

cwhanse commented 5 years ago

@tahentx I mostly agree with @mikofski. One primary purpose of pvlib is to provide model implementation that is faithful to the literature. In this case, a function that implement's Kimber's model (and only Kimber's model) is the first step.

Where I disagree is the suggestion to assign defaults to parameters. Where the paper recommends a parameter value, that value should be provided as the default. I didn't see that Kimber's paper provides values for any of the three parameters (in fact I didn't see an equation for the soiling loss percent in terms of soiling rate, cleaning threshold and grace period). If this is the case, I'd leave it to the user to assign appropriate values.

mikofski commented 5 years ago

Sorry I must have linked to the wrong paper, or maybe the Kimber model was internal only to SunPower. If there's no reference to the soiling loss equation, then I'm in favor of closing this in lieu of another soiling model.

mikofski commented 5 years ago

@cwhanse AFAIK that is the right paper, but perhaps it's not an equation, it's just an algorithm:

import pandas as pd
import datetime

def soiling_kimber_mitchell(rainfall_timeseries, threshold=6, soiling_rate=0.0015,
                            grace_period=14, max_soiling=0.3, manual_wash_dates=None):
    """
    Parameters
    ----------
    rainfall_timeseries : pandas.DataFrame
        a timeseries of rainfall in millimeters
    threshold : float, default 6[mm]
        the amount of rain in millimeters [mm] required to clean the panels
    soiling_rate: float, default 0.15%
        daily soiling rate, enter as fraction, not percent
    grace_period: int, default 14-days
        The time after a rainfall event when it's assumed the ground is damp, and
        so it's assumed there is no soiling. Change to smaller value for dry climate
    max_soiling: float, default 30%
        maximum soiling, soiling will accumulate until this value
    manual_wash_dates: sequence or None, default None
        A list or tuple of dates when the panels were manually cleaned. Note there
        is no grace period after a manual cleaning, so soiling begins to accumulate
        immediately after a manual cleaning, sorry :(

    Returns
    -------
    soiling : timeseries
        the daily soiling
    """

    # manual wash dates
    if manual_wash_dates is None:
        manual_wash_dates = []

    # resample rainfall as days by summing intermediate times
    rainfall = pd.resample(rainfall_timeseries, freq="D").sum()

    # soiling
    soiling = np.zeros_like(rainfall)

    # rainfall events that clean the panels
    rain_events = rainfall > thresh

    # loop over days
    for today in rainfall.index:

        # did rain exceed threshold?
        rain_exceed_thresh = rainfall[today] > threshold

        # if yes, then set soiling to zero
        if rain_exceed_thresh:
            soiling[today] = 0
            continue

        # start day of grace period
        start_day = today - grace_period

        # rainfall event during grace period?
        rain_in_grace_period = any( rain_events [ start_day : today ] )

        # if rain exceeded threshold during grace period,
        # assume ground is still damp, so no or v. low soiling
        if rain_in_grace_period:
            soiling[today] = 0
            continue

        # is this a manual wash date?
        if today in manual_wash_dates:
            soiling[today] = 0
            continue

        # so, it didn't rain enough to clean, it hasn't rained enough recently,
        # and we didn't manually clean panels, so soil them by adding daily
        # soiling rate to soiling from previous day
        total_soil = soiling[today - datetime.timedelta(days=1)] + soiling_rate

        # check if soiling has reached the maximum
        soiling[today] = max_soiling if (total_soil >= max_soiling) else total_soil 

    soiling = np.interp(rainfall_timeseries.index, rainfall.index, soiling)
    return pd.DataFrame(rainfall_timeseries.index, soiling)

WARNING: this is hacky bad psuedocode, needs to be rewritten by someone smart :)

mikofski commented 5 years ago

@tahentx also check out #739