-
Notifications
You must be signed in to change notification settings - Fork 985
/
soiling.py
211 lines (170 loc) · 8.27 KB
/
soiling.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# -*- coding: utf-8 -*-
"""
This module contains functions for soiling models
"""
import datetime
import numpy as np
import pandas as pd
from pvlib.tools import cosd
def hsu(rainfall, cleaning_threshold, tilt, pm2_5, pm10,
depo_veloc=None, rain_accum_period=pd.Timedelta('1h')):
"""
Calculates soiling ratio given particulate and rain data using the model
from Humboldt State University (HSU).
The HSU soiling model [1]_ returns the soiling ratio, a value between zero
and one which is equivalent to (1 - transmission loss). Therefore a soiling
ratio of 1.0 is equivalent to zero transmission loss.
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 pvlib.soiling.hsu function requires scipy.")
# never use mutable input arguments
if depo_veloc is None:
depo_veloc = {'2_5': 0.004, '10': 0.0009}
# 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)
# specify dtype so pandas doesn't assume object
mass_removed = pd.Series(index=rainfall.index, dtype='float64')
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
def kimber(rainfall, cleaning_threshold=6, soiling_loss_rate=0.0015,
grace_period=14, max_soiling=0.3, manual_wash_dates=None,
initial_soiling=0, rain_accum_period=24):
"""
Calculates fraction of energy lost due to soiling given rainfall data and
daily loss rate using the Kimber model.
Kimber soiling model [1]_ assumes soiling builds up at a daily rate unless
the daily rainfall is greater than a threshold. The model also assumes that
if daily rainfall has exceeded the threshold within a grace period, then
the ground is too damp to cause soiling build-up. The model also assumes
there is a maximum soiling build-up. Scheduled manual washes and rain
events are assumed to reset soiling to zero.
Parameters
----------
rainfall: pandas.Series
Accumulated rainfall at the end of each time period. [mm]
cleaning_threshold: float, default 6
Amount of daily rainfall required to clean the panels. [mm]
soiling_loss_rate: float, default 0.0015
Fraction of energy lost due to one day of soiling. [unitless]
grace_period : int, default 14
Number of days after a rainfall event when it's assumed the ground is
damp, and so it's assumed there is no soiling. [days]
max_soiling : float, default 0.3
Maximum fraction of energy lost due to soiling. Soiling will build up
until this value. [unitless]
manual_wash_dates : sequence or None, default None
List or tuple of dates as Python ``datetime.date`` when the panels were
washed manually. Note there is no grace period after a manual wash, so
soiling begins to build up immediately.
initial_soiling : float, default 0
Initial fraction of energy lost due to soiling at time zero in the
`rainfall` series input. [unitless]
rain_accum_period : int, default 24
Period for accumulating rainfall to check against `cleaning_threshold`.
The Kimber model defines this period as one day. [hours]
Returns
-------
pandas.Series
fraction of energy lost due to soiling, has same intervals as input
Notes
-----
The soiling loss rate depends on both the geographical region and the
soiling environment type. Rates measured by Kimber [1]_ are summarized in
the following table:
=================== ======= ========= ======================
Region/Environment Rural Suburban Urban/Highway/Airport
=================== ======= ========= ======================
Central Valley 0.0011 0.0019 0.0020
Northern CA 0.0011 0.0010 0.0016
Southern CA 0 0.0016 0.0019
Desert 0.0030 0.0030 0.0030
=================== ======= ========= ======================
Rainfall thresholds and grace periods may also vary by region. Please
consult [1]_ for more information.
References
----------
.. [1] "The Effect of Soiling on Large Grid-Connected Photovoltaic Systems
in California and the Southwest Region of the United States," Adrianne
Kimber, et al., IEEE 4th World Conference on Photovoltaic Energy
Conference, 2006, :doi:`10.1109/WCPEC.2006.279690`
"""
# convert rain_accum_period to timedelta
rain_accum_period = datetime.timedelta(hours=rain_accum_period)
# convert grace_period to timedelta
grace_period = datetime.timedelta(days=grace_period)
# get indices as numpy datetime64, calculate timestep as numpy timedelta64,
# and convert timestep to fraction of days
rain_index_vals = rainfall.index.values
timestep_interval = (rain_index_vals[1] - rain_index_vals[0])
day_fraction = timestep_interval / np.timedelta64(24, 'h')
# accumulate rainfall
accumulated_rainfall = rainfall.rolling(
rain_accum_period, closed='right').sum()
# soiling rate
soiling = np.ones_like(rainfall.values) * soiling_loss_rate * day_fraction
soiling[0] = initial_soiling
soiling = np.cumsum(soiling)
soiling = pd.Series(soiling, index=rainfall.index, name='soiling')
# rainfall events that clean the panels
rain_events = accumulated_rainfall > cleaning_threshold
# grace periods windows during which ground is assumed damp, so no soiling
grace_windows = rain_events.rolling(grace_period, closed='right').sum() > 0
# clean panels by subtracting soiling for indices in grace period windows
cleaning = pd.Series(float('NaN'), index=rainfall.index)
cleaning.iloc[0] = 0.0
cleaning[grace_windows] = soiling[grace_windows]
# manual wash dates
if manual_wash_dates is not None:
rain_tz = rainfall.index.tz
# convert manual wash dates to datetime index in the timezone of rain
manual_wash_dates = pd.DatetimeIndex(manual_wash_dates, tz=rain_tz)
cleaning[manual_wash_dates] = soiling[manual_wash_dates]
# remove soiling by foward filling cleaning where NaN
soiling -= cleaning.ffill()
# check if soiling has reached the maximum
return soiling.where(soiling < max_soiling, max_soiling)