Skip to content

Commit 04bd753

Browse files
authored
avoid runtime warnings from nan comparisons and divide by zero (#429)
* replace manual perez binning with np.digitize * update whatsnew * add invalid=ignore * refactor solis masking to avoid nan comparisons * avoid iam and adrinverter warnings. added tests to make sure it works * avoid sapm log warnings * avoid runtime warning in sapm aoi * add another adrinverter test, fix night bug * document adrinverter warnings * minimum numpy version to 1.10.1 * ignore over warnings in v_from_i * use specific names for mask arrays * comments, use errstate for clarity * ensure adrinverter handles nan inputs
1 parent 8a4aaca commit 04bd753

File tree

7 files changed

+123
-53
lines changed

7 files changed

+123
-53
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ install:
5353
- if [[ "$CONDA_ENV" == "py27-min" ]]; then
5454
pip uninstall numpy --yes;
5555
pip uninstall pandas --yes;
56-
pip install --no-cache-dir numpy==1.9.0;
56+
pip install --no-cache-dir numpy==1.10.1;
5757
pip install --no-cache-dir pandas==0.14.0;
5858
fi
5959
- conda list

docs/sphinx/source/whatsnew/v0.5.2.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Bug fixes
1717
* physicaliam now returns a Series if called with a Series as an
1818
argument. (:issue:`397`)
1919
* corrected docstring for irradiance.total_irrad (:issue: '423')
20+
* removed RuntimeWarnings due to divide by 0 or nans in input data within
21+
irradiance.perez, clearsky.simplified_solis, pvsystem.adrinverter,
22+
pvsystem.ashraeiam, pvsystem.physicaliam, pvsystem.sapm_aoi_loss,
23+
pvsystem.v_from_i. (:issue:`428`)
2024

2125
Documentation
2226
~~~~~~~~~~~~~

pvlib/clearsky.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -433,11 +433,7 @@ def simplified_solis(apparent_elevation, aod700=0.1, precipitable_water=1.,
433433
w = precipitable_water
434434

435435
# algorithm fails for pw < 0.2
436-
if np.isscalar(w):
437-
w = 0.2 if w < 0.2 else w
438-
else:
439-
w = w.copy()
440-
w[w < 0.2] = 0.2
436+
w = np.maximum(w, 0.2)
441437

442438
# this algorithm is reasonably fast already, but it could be made
443439
# faster by precalculating the powers of aod700, the log(p/p0), and
@@ -542,8 +538,10 @@ def _calc_taud(w, aod700, p):
542538
elif np.isscalar(aod700):
543539
aod700 = np.full_like(w, aod700)
544540

545-
aod700_mask = aod700 < 0.05
546-
aod700_mask = np.array([aod700_mask, ~aod700_mask], dtype=np.int)
541+
# set up nan-tolerant masks
542+
aod700_lt_0p05 = np.full_like(aod700, False, dtype='bool')
543+
np.less(aod700, 0.05, where=~np.isnan(aod700), out=aod700_lt_0p05)
544+
aod700_mask = np.array([aod700_lt_0p05, ~aod700_lt_0p05], dtype=np.int)
547545

548546
# create tuples of coefficients for
549547
# aod700 < 0.05, aod700 >= 0.05

pvlib/irradiance.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,8 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
995995
delta = dhi * airmass / dni_extra
996996

997997
# epsilon is the sky's "clearness"
998-
eps = ((dhi + dni) / dhi + kappa * (z ** 3)) / (1 + kappa * (z ** 3))
998+
with np.errstate(invalid='ignore'):
999+
eps = ((dhi + dni) / dhi + kappa * (z ** 3)) / (1 + kappa * (z ** 3))
9991000

10001001
# numpy indexing below will not work with a Series
10011002
if isinstance(eps, pd.Series):
@@ -1005,15 +1006,8 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
10051006
# rules. 1 = overcast ... 8 = clear (these names really only make
10061007
# sense for small zenith angles, but...) these values will
10071008
# eventually be used as indicies for coeffecient look ups
1008-
ebin = np.zeros_like(eps, dtype=np.int8)
1009-
ebin[eps < 1.065] = 1
1010-
ebin[(eps >= 1.065) & (eps < 1.23)] = 2
1011-
ebin[(eps >= 1.23) & (eps < 1.5)] = 3
1012-
ebin[(eps >= 1.5) & (eps < 1.95)] = 4
1013-
ebin[(eps >= 1.95) & (eps < 2.8)] = 5
1014-
ebin[(eps >= 2.8) & (eps < 4.5)] = 6
1015-
ebin[(eps >= 4.5) & (eps < 6.2)] = 7
1016-
ebin[eps >= 6.2] = 8
1009+
ebin = np.digitize(eps, (0., 1.065, 1.23, 1.5, 1.95, 2.8, 4.5, 6.2))
1010+
ebin[np.isnan(eps)] = 0
10171011

10181012
# correct for 0 indexing in coeffecient lookup
10191013
# later, ebin = -1 will yield nan coefficients
@@ -1333,7 +1327,7 @@ def dirindex(ghi, ghi_clearsky, dni_clearsky, zenith, times, pressure=101325.,
13331327
Determine DNI from GHI using the DIRINDEX model, which is a modification of
13341328
the DIRINT model with information from a clear sky model.
13351329
1336-
DIRINDEX [1] improves upon the DIRINT model by taking into account
1330+
DIRINDEX [1] improves upon the DIRINT model by taking into account
13371331
turbidity when used with the Ineichen clear sky model results.
13381332
13391333
Parameters
@@ -1396,7 +1390,7 @@ def dirindex(ghi, ghi_clearsky, dni_clearsky, zenith, times, pressure=101325.,
13961390
use_delta_kt_prime=use_delta_kt_prime,
13971391
temp_dew=temp_dew)
13981392

1399-
dni_dirint_clearsky = dirint(ghi_clearsky, zenith, times,
1393+
dni_dirint_clearsky = dirint(ghi_clearsky, zenith, times,
14001394
pressure=pressure,
14011395
use_delta_kt_prime=use_delta_kt_prime,
14021396
temp_dew=temp_dew)

pvlib/pvsystem.py

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -732,9 +732,10 @@ def ashraeiam(aoi, b=0.05):
732732
physicaliam
733733
'''
734734

735-
iam = 1 - b*((1/np.cos(np.radians(aoi)) - 1))
736-
737-
iam = np.where(np.abs(aoi) >= 90, 0, iam)
735+
iam = 1 - b * ((1 / np.cos(np.radians(aoi)) - 1))
736+
aoi_gte_90 = np.full_like(aoi, False, dtype='bool')
737+
np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)
738+
iam = np.where(aoi_gte_90, 0, iam)
738739
iam = np.maximum(0, iam)
739740

740741
if isinstance(iam, pd.Series):
@@ -836,16 +837,18 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):
836837
# after deducting the reflected portion of each
837838
iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero)
838839

839-
# angles near zero produce nan, but iam is defined as one
840-
small_angle = 1e-06
841-
iam = np.where(np.abs(aoi) < small_angle, 1.0, iam)
840+
with np.errstate(invalid='ignore'):
841+
# angles near zero produce nan, but iam is defined as one
842+
small_angle = 1e-06
843+
iam = np.where(np.abs(aoi) < small_angle, 1.0, iam)
842844

843-
# angles at 90 degrees can produce tiny negative values, which should be zero
844-
# this is a result of calculation precision rather than the physical model
845-
iam = np.where(iam < 0, 0, iam)
845+
# angles at 90 degrees can produce tiny negative values,
846+
# which should be zero. this is a result of calculation precision
847+
# rather than the physical model
848+
iam = np.where(iam < 0, 0, iam)
846849

847-
# for light coming from behind the plane, none can enter the module
848-
iam = np.where(aoi > 90, 0, iam)
850+
# for light coming from behind the plane, none can enter the module
851+
iam = np.where(aoi > 90, 0, iam)
849852

850853
if isinstance(aoi_input, pd.Series):
851854
iam = pd.Series(iam, index=aoi_input.index)
@@ -1296,12 +1299,27 @@ def sapm(effective_irradiance, temp_cell, module):
12961299
q = 1.60218e-19 # Elementary charge in units of coulombs
12971300
kb = 1.38066e-23 # Boltzmann's constant in units of J/K
12981301

1299-
Ee = effective_irradiance
1302+
# avoid problem with integer input
1303+
Ee = np.array(effective_irradiance, dtype='float64')
1304+
1305+
# set up masking for 0, positive, and nan inputs
1306+
Ee_gt_0 = np.full_like(Ee, False, dtype='bool')
1307+
Ee_eq_0 = np.full_like(Ee, False, dtype='bool')
1308+
notnan = ~np.isnan(Ee)
1309+
np.greater(Ee, 0, where=notnan, out=Ee_gt_0)
1310+
np.equal(Ee, 0, where=notnan, out=Ee_eq_0)
13001311

13011312
Bvmpo = module['Bvmpo'] + module['Mbvmp']*(1 - Ee)
13021313
Bvoco = module['Bvoco'] + module['Mbvoc']*(1 - Ee)
13031314
delta = module['N'] * kb * (temp_cell + 273.15) / q
13041315

1316+
# avoid repeated computation
1317+
logEe = np.full_like(Ee, np.nan)
1318+
np.log(Ee, where=Ee_gt_0, out=logEe)
1319+
logEe = np.where(Ee_eq_0, -np.inf, logEe)
1320+
# avoid repeated __getitem__
1321+
cells_in_series = module['Cells_in_Series']
1322+
13051323
out = OrderedDict()
13061324

13071325
out['i_sc'] = (
@@ -1312,13 +1330,13 @@ def sapm(effective_irradiance, temp_cell, module):
13121330
(1 + module['Aimp']*(temp_cell - T0)))
13131331

13141332
out['v_oc'] = np.maximum(0, (
1315-
module['Voco'] + module['Cells_in_Series']*delta*np.log(Ee) +
1333+
module['Voco'] + cells_in_series * delta * logEe +
13161334
Bvoco*(temp_cell - T0)))
13171335

13181336
out['v_mp'] = np.maximum(0, (
13191337
module['Vmpo'] +
1320-
module['C2']*module['Cells_in_Series']*delta*np.log(Ee) +
1321-
module['C3']*module['Cells_in_Series']*((delta*np.log(Ee)) ** 2) +
1338+
module['C2'] * cells_in_series * delta * logEe +
1339+
module['C3'] * cells_in_series * ((delta * logEe) ** 2) +
13221340
Bvmpo*(temp_cell - T0)))
13231341

13241342
out['p_mp'] = out['i_mp'] * out['v_mp']
@@ -1518,7 +1536,10 @@ def sapm_aoi_loss(aoi, module, upper=None):
15181536

15191537
aoi_loss = np.polyval(aoi_coeff, aoi)
15201538
aoi_loss = np.clip(aoi_loss, 0, upper)
1521-
aoi_loss = np.where(aoi < 0, 0, aoi_loss)
1539+
# nan tolerant masking
1540+
aoi_lt_0 = np.full_like(aoi, False, dtype='bool')
1541+
np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)
1542+
aoi_loss = np.where(aoi_lt_0, 0, aoi_loss)
15221543

15231544
if isinstance(aoi, pd.Series):
15241545
aoi_loss = pd.Series(aoi_loss, aoi.index)
@@ -1912,9 +1933,11 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current,
19121933
# Only compute using LambertW if there are cases with Gsh>0
19131934
if np.any(idx_p):
19141935
# LambertW argument, cannot be float128, may overflow to np.inf
1915-
argW = I0[idx_p] / (Gsh[idx_p]*a[idx_p]) * \
1916-
np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) /
1917-
(Gsh[idx_p]*a[idx_p]))
1936+
# overflow is explicitly handled below, so ignore warnings here
1937+
with np.errstate(over='ignore'):
1938+
argW = (I0[idx_p] / (Gsh[idx_p]*a[idx_p]) *
1939+
np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) /
1940+
(Gsh[idx_p]*a[idx_p])))
19181941

19191942
# lambertw typically returns complex value with zero imaginary part
19201943
# may overflow to np.inf
@@ -2274,22 +2297,39 @@ def adrinverter(v_dc, p_dc, inverter, vtol=0.10):
22742297
mppt_hi = inverter['MPPTHi']
22752298
mppt_low = inverter['MPPTLow']
22762299

2277-
v_lim_upper = np.nanmax([v_max, vdc_max, mppt_hi])*(1+vtol)
2278-
v_lim_lower = np.nanmax([v_min, mppt_low])*(1-vtol)
2279-
2280-
pdc = p_dc/p_nom
2281-
vdc = v_dc/v_nom
2282-
poly = np.array([pdc**0, pdc, pdc**2, vdc-1, pdc*(vdc-1),
2283-
pdc**2*(vdc-1), 1/vdc-1, pdc*(1./vdc-1),
2284-
pdc**2*(1./vdc-1)])
2300+
v_lim_upper = np.nanmax([v_max, vdc_max, mppt_hi]) * (1 + vtol)
2301+
v_lim_lower = np.nanmax([v_min, mppt_low]) * (1 - vtol)
2302+
2303+
pdc = p_dc / p_nom
2304+
vdc = v_dc / v_nom
2305+
# zero voltage will lead to division by zero, but since power is
2306+
# set to night time value later, these errors can be safely ignored
2307+
with np.errstate(invalid='ignore', divide='ignore'):
2308+
poly = np.array([pdc**0, # replace with np.ones_like?
2309+
pdc,
2310+
pdc**2,
2311+
vdc - 1,
2312+
pdc * (vdc - 1),
2313+
pdc**2 * (vdc - 1),
2314+
1. / vdc - 1, # divide by 0
2315+
pdc * (1. / vdc - 1), # invalid 0./0. --> nan
2316+
pdc**2 * (1. / vdc - 1)]) # divide by 0
22852317
p_loss = np.dot(np.array(ce_list), poly)
22862318
ac_power = p_nom * (pdc-p_loss)
2287-
p_nt = -1*np.absolute(p_nt)
2319+
p_nt = -1 * np.absolute(p_nt)
2320+
2321+
# set output to nan where input is outside of limits
2322+
# errstate silences case where input is nan
2323+
with np.errstate(invalid='ignore'):
2324+
invalid = (v_lim_upper < v_dc) | (v_dc < v_lim_lower)
2325+
ac_power = np.where(invalid, np.nan, ac_power)
2326+
2327+
# set night values
2328+
ac_power = np.where(vdc == 0, p_nt, ac_power)
2329+
ac_power = np.maximum(ac_power, p_nt)
22882330

2289-
ac_power = np.where((v_lim_upper < v_dc) | (v_dc < v_lim_lower),
2290-
np.nan, ac_power)
2291-
ac_power = np.where((ac_power < p_nt) | (vdc == 0), p_nt, ac_power)
2292-
ac_power = np.where(ac_power > pac_max, pac_max, ac_power)
2331+
# set max ac output
2332+
ac_power = np.minimum(ac_power, pac_max)
22932333

22942334
if isinstance(p_dc, pd.Series):
22952335
ac_power = pd.Series(ac_power, index=pdc.index)

pvlib/test/test_pvsystem.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ def test_ashraeiam():
101101
assert_allclose(iam, expected, equal_nan=True)
102102

103103

104+
@needs_numpy_1_10
105+
def test_ashraeiam_scalar():
106+
thetas = -45.
107+
iam = pvsystem.ashraeiam(thetas, .05)
108+
expected = 0.97928932
109+
assert_allclose(iam, expected, equal_nan=True)
110+
thetas = np.nan
111+
iam = pvsystem.ashraeiam(thetas, .05)
112+
expected = np.nan
113+
assert_allclose(iam, expected, equal_nan=True)
114+
115+
104116
@needs_numpy_1_10
105117
def test_PVSystem_ashraeiam():
106118
module_parameters = pd.Series({'b': 0.05})
@@ -127,6 +139,18 @@ def test_physicaliam():
127139
assert_series_equal(iam, expected)
128140

129141

142+
@needs_numpy_1_10
143+
def test_physicaliam_scalar():
144+
aoi = -45.
145+
iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4)
146+
expected = 0.98797788
147+
assert_allclose(iam, expected, equal_nan=True)
148+
aoi = np.nan
149+
iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4)
150+
expected = np.nan
151+
assert_allclose(iam, expected, equal_nan=True)
152+
153+
130154
@needs_numpy_1_10
131155
def test_PVSystem_physicaliam():
132156
module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526})
@@ -815,6 +839,16 @@ def test_adrinverter_float(sam_data):
815839
assert_allclose(pacs, 1161.5745)
816840

817841

842+
def test_adrinverter_invalid_and_night(sam_data):
843+
inverters = sam_data['adrinverter']
844+
testinv = 'Zigor__Sunzet_3_TL_US_240V__CEC_2011_'
845+
vdcs = np.array([39.873036, 0., np.nan, 420])
846+
pdcs = np.array([188.09182, 0., 420, np.nan])
847+
848+
pacs = pvsystem.adrinverter(vdcs, pdcs, inverters[testinv])
849+
assert_allclose(pacs, np.array([np.nan, -0.25, np.nan, np.nan]))
850+
851+
818852
def test_snlinverter(sam_data):
819853
inverters = sam_data['cecinverter']
820854
testinv = 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
MAINTAINER_EMAIL = '[email protected]'
4141
URL = 'https://github.com/pvlib/pvlib-python'
4242

43-
INSTALL_REQUIRES = ['numpy >= 1.9.0',
43+
INSTALL_REQUIRES = ['numpy >= 1.10.1',
4444
'pandas >= 0.14.0',
4545
'pytz',
4646
'six',

0 commit comments

Comments
 (0)