pvlib 0.10.3__py3-none-any.whl → 0.10.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pvlib/__init__.py +1 -0
- pvlib/bifacial/utils.py +2 -1
- pvlib/clearsky.py +7 -8
- pvlib/iam.py +3 -3
- pvlib/inverter.py +3 -3
- pvlib/iotools/__init__.py +2 -0
- pvlib/iotools/solargis.py +214 -0
- pvlib/iotools/solcast.py +2 -7
- pvlib/iotools/solrad.py +121 -23
- pvlib/iotools/srml.py +12 -12
- pvlib/iotools/surfrad.py +2 -2
- pvlib/irradiance.py +28 -22
- pvlib/location.py +3 -1
- pvlib/modelchain.py +10 -9
- pvlib/pvarray.py +127 -0
- pvlib/pvsystem.py +52 -43
- pvlib/scaling.py +4 -2
- pvlib/shading.py +110 -0
- pvlib/singlediode.py +37 -9
- pvlib/snow.py +3 -1
- pvlib/solarposition.py +38 -30
- pvlib/spa.py +3 -11
- pvlib/spectrum/mismatch.py +2 -1
- pvlib/temperature.py +3 -2
- pvlib/tests/bifacial/test_utils.py +6 -5
- pvlib/tests/conftest.py +13 -14
- pvlib/tests/iotools/test_sodapro.py +2 -1
- pvlib/tests/iotools/test_solargis.py +68 -0
- pvlib/tests/iotools/test_solcast.py +2 -2
- pvlib/tests/iotools/test_solrad.py +58 -7
- pvlib/tests/iotools/test_srml.py +7 -14
- pvlib/tests/test_clearsky.py +1 -1
- pvlib/tests/test_irradiance.py +24 -8
- pvlib/tests/test_location.py +1 -1
- pvlib/tests/test_modelchain.py +37 -26
- pvlib/tests/test_pvarray.py +25 -0
- pvlib/tests/test_pvsystem.py +76 -104
- pvlib/tests/test_shading.py +130 -11
- pvlib/tests/test_singlediode.py +68 -10
- pvlib/tests/test_snow.py +1 -1
- pvlib/tests/test_solarposition.py +121 -7
- pvlib/tests/test_spa.py +5 -15
- pvlib/tests/test_temperature.py +4 -4
- pvlib/tests/test_tracking.py +2 -2
- pvlib/tracking.py +8 -38
- pvlib/version.py +1 -5
- {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/METADATA +9 -33
- {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/RECORD +52 -51
- {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/WHEEL +1 -1
- pvlib/spa_c_files/SPA_NOTICE.md +0 -39
- {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/AUTHORS.md +0 -0
- {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/LICENSE +0 -0
- {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/top_level.txt +0 -0
pvlib/singlediode.py
CHANGED
|
@@ -58,7 +58,7 @@ def estimate_voc(photocurrent, saturation_current, nNsVth):
|
|
|
58
58
|
|
|
59
59
|
def bishop88(diode_voltage, photocurrent, saturation_current,
|
|
60
60
|
resistance_series, resistance_shunt, nNsVth, d2mutau=0,
|
|
61
|
-
NsVbi=np.
|
|
61
|
+
NsVbi=np.inf, breakdown_factor=0., breakdown_voltage=-5.5,
|
|
62
62
|
breakdown_exp=3.28, gradients=False):
|
|
63
63
|
r"""
|
|
64
64
|
Explicit calculation of points on the IV curve described by the single
|
|
@@ -206,7 +206,7 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
|
|
|
206
206
|
|
|
207
207
|
def bishop88_i_from_v(voltage, photocurrent, saturation_current,
|
|
208
208
|
resistance_series, resistance_shunt, nNsVth,
|
|
209
|
-
d2mutau=0, NsVbi=np.
|
|
209
|
+
d2mutau=0, NsVbi=np.inf, breakdown_factor=0.,
|
|
210
210
|
breakdown_voltage=-5.5, breakdown_exp=3.28,
|
|
211
211
|
method='newton', method_kwargs=None):
|
|
212
212
|
"""
|
|
@@ -285,6 +285,12 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
|
|
|
285
285
|
|
|
286
286
|
>>> i, method_output = bishop88_i_from_v(0.0, **args, method='newton',
|
|
287
287
|
... method_kwargs={'full_output': True})
|
|
288
|
+
|
|
289
|
+
References
|
|
290
|
+
----------
|
|
291
|
+
.. [1] "Computer simulation of the effects of electrical mismatches in
|
|
292
|
+
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
|
|
293
|
+
:doi:`10.1016/0379-6787(88)90059-2`
|
|
288
294
|
"""
|
|
289
295
|
# collect args
|
|
290
296
|
args = (photocurrent, saturation_current,
|
|
@@ -304,6 +310,9 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
|
|
|
304
310
|
if method == 'brentq':
|
|
305
311
|
# first bound the search using voc
|
|
306
312
|
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
|
|
313
|
+
# start iteration slightly less than NsVbi when voc_est > NsVbi, to
|
|
314
|
+
# avoid the asymptote at NsVbi
|
|
315
|
+
xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi)
|
|
307
316
|
|
|
308
317
|
# brentq only works with scalar inputs, so we need a set up function
|
|
309
318
|
# and np.vectorize to repeatedly call the optimizer with the right
|
|
@@ -317,7 +326,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
|
|
|
317
326
|
**method_kwargs)
|
|
318
327
|
|
|
319
328
|
vd_from_brent_vectorized = np.vectorize(vd_from_brent)
|
|
320
|
-
vd = vd_from_brent_vectorized(
|
|
329
|
+
vd = vd_from_brent_vectorized(xp, voltage, *args)
|
|
321
330
|
elif method == 'newton':
|
|
322
331
|
x0, (voltage, *args), method_kwargs = \
|
|
323
332
|
_prepare_newton_inputs(voltage, (voltage, *args), method_kwargs)
|
|
@@ -338,7 +347,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
|
|
|
338
347
|
|
|
339
348
|
def bishop88_v_from_i(current, photocurrent, saturation_current,
|
|
340
349
|
resistance_series, resistance_shunt, nNsVth,
|
|
341
|
-
d2mutau=0, NsVbi=np.
|
|
350
|
+
d2mutau=0, NsVbi=np.inf, breakdown_factor=0.,
|
|
342
351
|
breakdown_voltage=-5.5, breakdown_exp=3.28,
|
|
343
352
|
method='newton', method_kwargs=None):
|
|
344
353
|
"""
|
|
@@ -417,6 +426,12 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
|
|
|
417
426
|
|
|
418
427
|
>>> v, method_output = bishop88_v_from_i(0.0, **args, method='newton',
|
|
419
428
|
... method_kwargs={'full_output': True})
|
|
429
|
+
|
|
430
|
+
References
|
|
431
|
+
----------
|
|
432
|
+
.. [1] "Computer simulation of the effects of electrical mismatches in
|
|
433
|
+
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
|
|
434
|
+
:doi:`10.1016/0379-6787(88)90059-2`
|
|
420
435
|
"""
|
|
421
436
|
# collect args
|
|
422
437
|
args = (photocurrent, saturation_current,
|
|
@@ -431,6 +446,9 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
|
|
|
431
446
|
|
|
432
447
|
# first bound the search using voc
|
|
433
448
|
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
|
|
449
|
+
# start iteration slightly less than NsVbi when voc_est > NsVbi, to avoid
|
|
450
|
+
# the asymptote at NsVbi
|
|
451
|
+
xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi)
|
|
434
452
|
|
|
435
453
|
def fi(x, i, *a):
|
|
436
454
|
# calculate current residual given diode voltage "x"
|
|
@@ -449,10 +467,10 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
|
|
|
449
467
|
**method_kwargs)
|
|
450
468
|
|
|
451
469
|
vd_from_brent_vectorized = np.vectorize(vd_from_brent)
|
|
452
|
-
vd = vd_from_brent_vectorized(
|
|
470
|
+
vd = vd_from_brent_vectorized(xp, current, *args)
|
|
453
471
|
elif method == 'newton':
|
|
454
472
|
x0, (current, *args), method_kwargs = \
|
|
455
|
-
_prepare_newton_inputs(
|
|
473
|
+
_prepare_newton_inputs(xp, (current, *args), method_kwargs)
|
|
456
474
|
vd = newton(func=lambda x, *a: fi(x, current, *a), x0=x0,
|
|
457
475
|
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3],
|
|
458
476
|
args=args, **method_kwargs)
|
|
@@ -469,7 +487,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
|
|
|
469
487
|
|
|
470
488
|
|
|
471
489
|
def bishop88_mpp(photocurrent, saturation_current, resistance_series,
|
|
472
|
-
resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.
|
|
490
|
+
resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.inf,
|
|
473
491
|
breakdown_factor=0., breakdown_voltage=-5.5,
|
|
474
492
|
breakdown_exp=3.28, method='newton', method_kwargs=None):
|
|
475
493
|
"""
|
|
@@ -547,6 +565,12 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
|
|
|
547
565
|
|
|
548
566
|
>>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(**args,
|
|
549
567
|
... method='newton', method_kwargs={'full_output': True})
|
|
568
|
+
|
|
569
|
+
References
|
|
570
|
+
----------
|
|
571
|
+
.. [1] "Computer simulation of the effects of electrical mismatches in
|
|
572
|
+
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
|
|
573
|
+
:doi:`10.1016/0379-6787(88)90059-2`
|
|
550
574
|
"""
|
|
551
575
|
# collect args
|
|
552
576
|
args = (photocurrent, saturation_current,
|
|
@@ -561,6 +585,9 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
|
|
|
561
585
|
|
|
562
586
|
# first bound the search using voc
|
|
563
587
|
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
|
|
588
|
+
# start iteration slightly less than NsVbi when voc_est > NsVbi, to avoid
|
|
589
|
+
# the asymptote at NsVbi
|
|
590
|
+
xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi)
|
|
564
591
|
|
|
565
592
|
def fmpp(x, *a):
|
|
566
593
|
return bishop88(x, *a, gradients=True)[6]
|
|
@@ -574,12 +601,13 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
|
|
|
574
601
|
vbr_a, vbr, vbr_exp),
|
|
575
602
|
**method_kwargs)
|
|
576
603
|
)
|
|
577
|
-
vd = vec_fun(
|
|
604
|
+
vd = vec_fun(xp, *args)
|
|
578
605
|
elif method == 'newton':
|
|
579
606
|
# make sure all args are numpy arrays if max size > 1
|
|
580
607
|
# if voc_est is an array, then make a copy to use for initial guess, v0
|
|
608
|
+
|
|
581
609
|
x0, args, method_kwargs = \
|
|
582
|
-
_prepare_newton_inputs(
|
|
610
|
+
_prepare_newton_inputs(xp, args, method_kwargs)
|
|
583
611
|
vd = newton(func=fmpp, x0=x0,
|
|
584
612
|
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7],
|
|
585
613
|
args=args, **method_kwargs)
|
pvlib/snow.py
CHANGED
|
@@ -222,7 +222,9 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
|
|
|
222
222
|
string_factor=1.0, angle_of_repose=40):
|
|
223
223
|
'''
|
|
224
224
|
Calculates monthly snow loss based on the Townsend monthly snow loss
|
|
225
|
-
model
|
|
225
|
+
model.
|
|
226
|
+
|
|
227
|
+
This model is described in [1]_.
|
|
226
228
|
|
|
227
229
|
Parameters
|
|
228
230
|
----------
|
pvlib/solarposition.py
CHANGED
|
@@ -28,9 +28,6 @@ from pvlib import atmosphere
|
|
|
28
28
|
from pvlib.tools import datetime_to_djd, djd_to_datetime
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
NS_PER_HR = 1.e9 * 3600. # nanoseconds per hour
|
|
32
|
-
|
|
33
|
-
|
|
34
31
|
def get_solarposition(time, latitude, longitude,
|
|
35
32
|
altitude=None, pressure=None,
|
|
36
33
|
method='nrel_numpy',
|
|
@@ -89,7 +86,7 @@ def get_solarposition(time, latitude, longitude,
|
|
|
89
86
|
solar radiation applications. Solar Energy, vol. 81, no. 6, p. 838,
|
|
90
87
|
2007.
|
|
91
88
|
|
|
92
|
-
.. [3] NREL SPA code:
|
|
89
|
+
.. [3] NREL SPA code: https://midcdmz.nrel.gov/spa/
|
|
93
90
|
"""
|
|
94
91
|
|
|
95
92
|
if altitude is None and pressure is None:
|
|
@@ -132,7 +129,7 @@ def get_solarposition(time, latitude, longitude,
|
|
|
132
129
|
def spa_c(time, latitude, longitude, pressure=101325, altitude=0,
|
|
133
130
|
temperature=12, delta_t=67.0,
|
|
134
131
|
raw_spa_output=False):
|
|
135
|
-
"""
|
|
132
|
+
r"""
|
|
136
133
|
Calculate the solar position using the C implementation of the NREL
|
|
137
134
|
SPA code.
|
|
138
135
|
|
|
@@ -161,7 +158,7 @@ def spa_c(time, latitude, longitude, pressure=101325, altitude=0,
|
|
|
161
158
|
Temperature in C
|
|
162
159
|
delta_t : float, default 67.0
|
|
163
160
|
Difference between terrestrial time and UT1.
|
|
164
|
-
USNO has previous values and predictions.
|
|
161
|
+
USNO has previous values and predictions [3]_.
|
|
165
162
|
raw_spa_output : bool, default False
|
|
166
163
|
If true, returns the raw SPA output.
|
|
167
164
|
|
|
@@ -177,17 +174,16 @@ def spa_c(time, latitude, longitude, pressure=101325, altitude=0,
|
|
|
177
174
|
|
|
178
175
|
References
|
|
179
176
|
----------
|
|
180
|
-
.. [1] NREL SPA reference:
|
|
181
|
-
http://rredc.nrel.gov/solar/codesandalgorithms/spa/
|
|
182
|
-
NREL SPA C files: https://midcdmz.nrel.gov/spa/
|
|
177
|
+
.. [1] NREL SPA reference: https://midcdmz.nrel.gov/spa/
|
|
183
178
|
|
|
184
179
|
Note: The ``timezone`` field in the SPA C files is replaced with
|
|
185
180
|
``time_zone`` to avoid a nameclash with the function ``__timezone`` that is
|
|
186
181
|
redefined by Python>=3.5. This issue is
|
|
187
182
|
`Python bug 24643 <https://bugs.python.org/issue24643>`_.
|
|
188
183
|
|
|
189
|
-
.. [2]
|
|
190
|
-
|
|
184
|
+
.. [2] Delta T: https://en.wikipedia.org/wiki/%CE%94T_(timekeeping)
|
|
185
|
+
|
|
186
|
+
.. [3] USNO delta T: https://maia.usno.navy.mil/products/deltaT
|
|
191
187
|
|
|
192
188
|
See also
|
|
193
189
|
--------
|
|
@@ -274,6 +270,19 @@ def _spa_python_import(how):
|
|
|
274
270
|
return spa
|
|
275
271
|
|
|
276
272
|
|
|
273
|
+
def _datetime_to_unixtime(dtindex):
|
|
274
|
+
# convert a pandas datetime index to unixtime, making sure to handle
|
|
275
|
+
# different pandas units (ns, us, etc) and time zones correctly
|
|
276
|
+
if dtindex.tz is not None:
|
|
277
|
+
# epoch is 1970-01-01 00:00 UTC, but we need to match the input tz
|
|
278
|
+
# for compatibility with older pandas versions (e.g. v1.3.5)
|
|
279
|
+
epoch = pd.Timestamp("1970-01-01", tz="UTC").tz_convert(dtindex.tz)
|
|
280
|
+
else:
|
|
281
|
+
epoch = pd.Timestamp("1970-01-01")
|
|
282
|
+
|
|
283
|
+
return np.array((dtindex - epoch) / pd.Timedelta("1s"))
|
|
284
|
+
|
|
285
|
+
|
|
277
286
|
def spa_python(time, latitude, longitude,
|
|
278
287
|
altitude=0, pressure=101325, temperature=12, delta_t=67.0,
|
|
279
288
|
atmos_refract=None, how='numpy', numthreads=4):
|
|
@@ -344,7 +353,7 @@ def spa_python(time, latitude, longitude,
|
|
|
344
353
|
2007.
|
|
345
354
|
|
|
346
355
|
.. [3] USNO delta T:
|
|
347
|
-
|
|
356
|
+
https://maia.usno.navy.mil/products/deltaT
|
|
348
357
|
|
|
349
358
|
See also
|
|
350
359
|
--------
|
|
@@ -366,7 +375,7 @@ def spa_python(time, latitude, longitude,
|
|
|
366
375
|
except (TypeError, ValueError):
|
|
367
376
|
time = pd.DatetimeIndex([time, ])
|
|
368
377
|
|
|
369
|
-
unixtime =
|
|
378
|
+
unixtime = _datetime_to_unixtime(time)
|
|
370
379
|
|
|
371
380
|
spa = _spa_python_import(how)
|
|
372
381
|
|
|
@@ -445,7 +454,7 @@ def sun_rise_set_transit_spa(times, latitude, longitude, how='numpy',
|
|
|
445
454
|
|
|
446
455
|
# must convert to midnight UTC on day of interest
|
|
447
456
|
utcday = pd.DatetimeIndex(times.date).tz_localize('UTC')
|
|
448
|
-
unixtime =
|
|
457
|
+
unixtime = _datetime_to_unixtime(utcday)
|
|
449
458
|
|
|
450
459
|
spa = _spa_python_import(how)
|
|
451
460
|
|
|
@@ -1001,7 +1010,7 @@ def nrel_earthsun_distance(time, how='numpy', delta_t=67.0, numthreads=4):
|
|
|
1001
1010
|
except (TypeError, ValueError):
|
|
1002
1011
|
time = pd.DatetimeIndex([time, ])
|
|
1003
1012
|
|
|
1004
|
-
unixtime =
|
|
1013
|
+
unixtime = _datetime_to_unixtime(time)
|
|
1005
1014
|
|
|
1006
1015
|
spa = _spa_python_import(how)
|
|
1007
1016
|
|
|
@@ -1327,9 +1336,9 @@ def solar_zenith_analytical(latitude, hourangle, declination):
|
|
|
1327
1336
|
.. [4] `Wikipedia: Solar Zenith Angle
|
|
1328
1337
|
<https://en.wikipedia.org/wiki/Solar_zenith_angle>`_
|
|
1329
1338
|
|
|
1330
|
-
.. [5] `PVCDROM:
|
|
1331
|
-
<
|
|
1332
|
-
|
|
1339
|
+
.. [5] `PVCDROM: Elevation Angle
|
|
1340
|
+
<https://www.pveducation.org/pvcdrom/properties-of-sunlight/
|
|
1341
|
+
elevation-angle>`_
|
|
1333
1342
|
|
|
1334
1343
|
See Also
|
|
1335
1344
|
--------
|
|
@@ -1378,11 +1387,13 @@ def hour_angle(times, longitude, equation_of_time):
|
|
|
1378
1387
|
equation_of_time_spencer71
|
|
1379
1388
|
equation_of_time_pvcdrom
|
|
1380
1389
|
"""
|
|
1381
|
-
naive_times = times.tz_localize(None) # naive but still localized
|
|
1382
1390
|
# hours - timezone = (times - normalized_times) - (naive_times - times)
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1391
|
+
if times.tz is None:
|
|
1392
|
+
times = times.tz_localize('utc')
|
|
1393
|
+
tzs = np.array([ts.utcoffset().total_seconds() for ts in times]) / 3600
|
|
1394
|
+
|
|
1395
|
+
hrs_minus_tzs = (times - times.normalize()) / pd.Timedelta('1h') - tzs
|
|
1396
|
+
|
|
1386
1397
|
# ensure array return instead of a version-dependent pandas <T>Index
|
|
1387
1398
|
return np.asarray(
|
|
1388
1399
|
15. * (hrs_minus_tzs - 12.) + longitude + equation_of_time / 4.)
|
|
@@ -1390,9 +1401,9 @@ def hour_angle(times, longitude, equation_of_time):
|
|
|
1390
1401
|
|
|
1391
1402
|
def _hour_angle_to_hours(times, hourangle, longitude, equation_of_time):
|
|
1392
1403
|
"""converts hour angles in degrees to hours as a numpy array"""
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1404
|
+
if times.tz is None:
|
|
1405
|
+
times = times.tz_localize('utc')
|
|
1406
|
+
tzs = np.array([ts.utcoffset().total_seconds() for ts in times]) / 3600
|
|
1396
1407
|
hours = (hourangle - longitude - equation_of_time / 4.) / 15. + 12. + tzs
|
|
1397
1408
|
return np.asarray(hours)
|
|
1398
1409
|
|
|
@@ -1406,16 +1417,13 @@ def _local_times_from_hours_since_midnight(times, hours):
|
|
|
1406
1417
|
# normalize local, naive times to previous midnight and add the hours until
|
|
1407
1418
|
# sunrise, sunset, and transit
|
|
1408
1419
|
return pd.DatetimeIndex(
|
|
1409
|
-
|
|
1410
|
-
(hours * NS_PER_HR).astype(np.int64)).astype('datetime64[ns]'),
|
|
1411
|
-
tz=tz_info)
|
|
1420
|
+
naive_times.normalize() + pd.to_timedelta(hours, unit='h'), tz=tz_info)
|
|
1412
1421
|
|
|
1413
1422
|
|
|
1414
1423
|
def _times_to_hours_after_local_midnight(times):
|
|
1415
1424
|
"""convert local pandas datetime indices to array of hours as floats"""
|
|
1416
1425
|
times = times.tz_localize(None)
|
|
1417
|
-
hrs =
|
|
1418
|
-
times.view(np.int64) - times.normalize().view(np.int64))
|
|
1426
|
+
hrs = (times - times.normalize()) / pd.Timedelta('1h')
|
|
1419
1427
|
return np.array(hrs)
|
|
1420
1428
|
|
|
1421
1429
|
|
pvlib/spa.py
CHANGED
|
@@ -21,23 +21,15 @@ def nocompile(*args, **kwargs):
|
|
|
21
21
|
|
|
22
22
|
if os.getenv('PVLIB_USE_NUMBA', '0') != '0':
|
|
23
23
|
try:
|
|
24
|
-
from numba import jit
|
|
24
|
+
from numba import jit
|
|
25
25
|
except ImportError:
|
|
26
26
|
warnings.warn('Could not import numba, falling back to numpy ' +
|
|
27
27
|
'calculation')
|
|
28
28
|
jcompile = nocompile
|
|
29
29
|
USE_NUMBA = False
|
|
30
30
|
else:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# need at least numba >= 0.17.0
|
|
34
|
-
jcompile = jit
|
|
35
|
-
USE_NUMBA = True
|
|
36
|
-
else:
|
|
37
|
-
warnings.warn('Numba version must be >= 0.17.0, falling back to ' +
|
|
38
|
-
'numpy')
|
|
39
|
-
jcompile = nocompile
|
|
40
|
-
USE_NUMBA = False
|
|
31
|
+
jcompile = jit
|
|
32
|
+
USE_NUMBA = True
|
|
41
33
|
else:
|
|
42
34
|
jcompile = nocompile
|
|
43
35
|
USE_NUMBA = False
|
pvlib/spectrum/mismatch.py
CHANGED
|
@@ -6,6 +6,7 @@ import pvlib
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
from scipy.interpolate import interp1d
|
|
9
|
+
from scipy.integrate import trapezoid
|
|
9
10
|
import os
|
|
10
11
|
|
|
11
12
|
from warnings import warn
|
|
@@ -224,7 +225,7 @@ def calc_spectral_mismatch_field(sr, e_sun, e_ref=None):
|
|
|
224
225
|
|
|
225
226
|
# a helper function to make usable fraction calculations more readable
|
|
226
227
|
def integrate(e):
|
|
227
|
-
return
|
|
228
|
+
return trapezoid(e, x=e.T.index, axis=-1)
|
|
228
229
|
|
|
229
230
|
# calculate usable fractions
|
|
230
231
|
uf_sun = integrate(e_sun * sr_sun) / integrate(e_sun)
|
pvlib/temperature.py
CHANGED
|
@@ -362,8 +362,9 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
|
|
|
362
362
|
|
|
363
363
|
References
|
|
364
364
|
----------
|
|
365
|
-
.. [1] "PVsyst
|
|
366
|
-
|
|
365
|
+
.. [1] "PVsyst 7 Help", [Online]. Available:
|
|
366
|
+
https://www.pvsyst.com/help/index.html?thermal_loss.htm.
|
|
367
|
+
[Accessed: 30-Jan-2024].
|
|
367
368
|
|
|
368
369
|
.. [2] Faiman, D. (2008). "Assessing the outdoor operating temperature of
|
|
369
370
|
photovoltaic modules." Progress in Photovoltaics 16(4): 307-315.
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
from pvlib.bifacial import utils
|
|
7
7
|
from pvlib.shading import masking_angle, ground_angle
|
|
8
8
|
from pvlib.tools import cosd
|
|
9
|
+
from scipy.integrate import trapezoid
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@pytest.fixture
|
|
@@ -99,7 +100,7 @@ def test_vf_ground_sky_2d_integ(test_system_fixed_tilt, vectorize):
|
|
|
99
100
|
vf_integ = utils.vf_ground_sky_2d_integ(
|
|
100
101
|
ts['rotation'], ts['gcr'], ts['height'], ts['pitch'],
|
|
101
102
|
max_rows=1, npoints=3, vectorize=vectorize)
|
|
102
|
-
expected_vf_integ =
|
|
103
|
+
expected_vf_integ = trapezoid(vfs_gnd_sky, pts, axis=0)
|
|
103
104
|
assert np.isclose(vf_integ, expected_vf_integ, rtol=0.1)
|
|
104
105
|
|
|
105
106
|
|
|
@@ -134,7 +135,7 @@ def test_vf_row_sky_2d_integ(test_system_fixed_tilt):
|
|
|
134
135
|
x = np.arange(fx0[1], fx1[1], 1e-4)
|
|
135
136
|
phi_y = masking_angle(ts['surface_tilt'], ts['gcr'], x)
|
|
136
137
|
y = 0.5 * (1 + cosd(ts['surface_tilt'] + phi_y))
|
|
137
|
-
y1 =
|
|
138
|
+
y1 = trapezoid(y, x) / (fx1[1] - fx0[1])
|
|
138
139
|
expected = np.array([y0, y1])
|
|
139
140
|
assert np.allclose(vf, expected, rtol=1e-3)
|
|
140
141
|
# with defaults (0, 1)
|
|
@@ -142,7 +143,7 @@ def test_vf_row_sky_2d_integ(test_system_fixed_tilt):
|
|
|
142
143
|
x = np.arange(0, 1, 1e-4)
|
|
143
144
|
phi_y = masking_angle(ts['surface_tilt'], ts['gcr'], x)
|
|
144
145
|
y = 0.5 * (1 + cosd(ts['surface_tilt'] + phi_y))
|
|
145
|
-
y1 =
|
|
146
|
+
y1 = trapezoid(y, x) / (1 - 0)
|
|
146
147
|
assert np.allclose(vf, y1, rtol=1e-3)
|
|
147
148
|
|
|
148
149
|
|
|
@@ -179,7 +180,7 @@ def test_vf_ground_2d_integ(test_system_fixed_tilt):
|
|
|
179
180
|
x = np.arange(fx0[1], fx1[1], 1e-4)
|
|
180
181
|
phi_y = ground_angle(ts['surface_tilt'], ts['gcr'], x)
|
|
181
182
|
y = 0.5 * (1 - cosd(phi_y - ts['surface_tilt']))
|
|
182
|
-
y1 =
|
|
183
|
+
y1 = trapezoid(y, x) / (fx1[1] - fx0[1])
|
|
183
184
|
expected = np.array([y0, y1])
|
|
184
185
|
assert np.allclose(vf, expected, rtol=1e-2)
|
|
185
186
|
# with defaults (0, 1)
|
|
@@ -187,5 +188,5 @@ def test_vf_ground_2d_integ(test_system_fixed_tilt):
|
|
|
187
188
|
x = np.arange(0, 1, 1e-4)
|
|
188
189
|
phi_y = ground_angle(ts['surface_tilt'], ts['gcr'], x)
|
|
189
190
|
y = 0.5 * (1 - cosd(phi_y - ts['surface_tilt']))
|
|
190
|
-
y1 =
|
|
191
|
+
y1 = trapezoid(y, x) / (1 - 0)
|
|
191
192
|
assert np.allclose(vf, y1, rtol=1e-2)
|
pvlib/tests/conftest.py
CHANGED
|
@@ -119,7 +119,7 @@ requires_statsmodels = pytest.mark.skipif(
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
try:
|
|
122
|
-
import ephem
|
|
122
|
+
import ephem # noqa: F401
|
|
123
123
|
has_ephem = True
|
|
124
124
|
except ImportError:
|
|
125
125
|
has_ephem = False
|
|
@@ -129,7 +129,7 @@ requires_ephem = pytest.mark.skipif(not has_ephem, reason='requires ephem')
|
|
|
129
129
|
|
|
130
130
|
def has_spa_c():
|
|
131
131
|
try:
|
|
132
|
-
from pvlib.spa_c_files.spa_py import spa_calc
|
|
132
|
+
from pvlib.spa_c_files.spa_py import spa_calc # noqa: F401
|
|
133
133
|
except ImportError:
|
|
134
134
|
return False
|
|
135
135
|
else:
|
|
@@ -139,20 +139,14 @@ def has_spa_c():
|
|
|
139
139
|
requires_spa_c = pytest.mark.skipif(not has_spa_c(), reason="requires spa_c")
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
else:
|
|
148
|
-
vers = numba.__version__.split('.')
|
|
149
|
-
if int(vers[0] + vers[1]) < 17:
|
|
150
|
-
return False
|
|
151
|
-
else:
|
|
152
|
-
return True
|
|
142
|
+
try:
|
|
143
|
+
import numba # noqa: F401
|
|
144
|
+
has_numba = True
|
|
145
|
+
except ImportError:
|
|
146
|
+
has_numba = False
|
|
153
147
|
|
|
154
148
|
|
|
155
|
-
requires_numba = pytest.mark.skipif(not has_numba
|
|
149
|
+
requires_numba = pytest.mark.skipif(not has_numba, reason="requires numba")
|
|
156
150
|
|
|
157
151
|
|
|
158
152
|
try:
|
|
@@ -174,6 +168,11 @@ except ImportError:
|
|
|
174
168
|
requires_pysam = pytest.mark.skipif(not has_pysam, reason="requires PySAM")
|
|
175
169
|
|
|
176
170
|
|
|
171
|
+
has_pandas_2_0 = Version(pd.__version__) >= Version("2.0.0")
|
|
172
|
+
requires_pandas_2_0 = pytest.mark.skipif(not has_pandas_2_0,
|
|
173
|
+
reason="requires pandas>=2.0.0")
|
|
174
|
+
|
|
175
|
+
|
|
177
176
|
@pytest.fixture()
|
|
178
177
|
def golden():
|
|
179
178
|
return Location(39.742476, -105.1786, 'America/Denver', 1830.14)
|
|
@@ -17,7 +17,8 @@ testfile_radiation_verbose = DATA_DIR / 'cams_radiation_1min_verbose.csv'
|
|
|
17
17
|
testfile_radiation_monthly = DATA_DIR / 'cams_radiation_monthly.csv'
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
index_verbose = pd.date_range('2020-06-01 12', periods=4, freq='
|
|
20
|
+
index_verbose = pd.date_range('2020-06-01 12', periods=4, freq='1min',
|
|
21
|
+
tz='UTC')
|
|
21
22
|
index_monthly = pd.date_range('2020-01-01', periods=4, freq='1M')
|
|
22
23
|
|
|
23
24
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
import pvlib
|
|
4
|
+
import requests
|
|
5
|
+
from ..conftest import (RERUNS, RERUNS_DELAY, assert_frame_equal,
|
|
6
|
+
assert_index_equal)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def hourly_index():
|
|
11
|
+
hourly_index = pd.date_range(start='2022-01-01 00:30+01:00', freq='60min',
|
|
12
|
+
periods=24, name='dateTime')
|
|
13
|
+
hourly_index.freq = None
|
|
14
|
+
return hourly_index
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def hourly_index_start_utc():
|
|
19
|
+
hourly_index_left_utc = pd.date_range(
|
|
20
|
+
start='2023-01-01 00:00+00:00', freq='30min', periods=24*2,
|
|
21
|
+
name='dateTime')
|
|
22
|
+
hourly_index_left_utc.freq = None
|
|
23
|
+
return hourly_index_left_utc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def hourly_dataframe(hourly_index):
|
|
28
|
+
ghi = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 73.0, 152.0, 141.0, 105.0,
|
|
29
|
+
62.0, 65.0, 62.0, 11.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
|
30
|
+
dni = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0, 233.0, 301.0, 136.0, 32.0,
|
|
31
|
+
0.0, 3.0, 77.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
|
32
|
+
return pd.DataFrame(data={'ghi': ghi, 'dni': dni}, index=hourly_index)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.remote_data
|
|
36
|
+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
|
|
37
|
+
def test_get_solargis(hourly_dataframe):
|
|
38
|
+
data, meta = pvlib.iotools.get_solargis(
|
|
39
|
+
latitude=48.61259, longitude=20.827079,
|
|
40
|
+
start='2022-01-01', end='2022-01-01',
|
|
41
|
+
tz='GMT+01', variables=['GHI', 'DNI'],
|
|
42
|
+
time_resolution='HOURLY', api_key='demo')
|
|
43
|
+
assert_frame_equal(data, hourly_dataframe)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.remote_data
|
|
47
|
+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
|
|
48
|
+
def test_get_solargis_utc_start_timestamp(hourly_index_start_utc):
|
|
49
|
+
data, meta = pvlib.iotools.get_solargis(
|
|
50
|
+
latitude=48.61259, longitude=20.827079,
|
|
51
|
+
start='2023-01-01', end='2023-01-01',
|
|
52
|
+
variables=['GTI'],
|
|
53
|
+
timestamp_type='start',
|
|
54
|
+
time_resolution='MIN_30',
|
|
55
|
+
map_variables=False, api_key='demo')
|
|
56
|
+
assert 'GTI' in data.columns # assert that variables aren't mapped
|
|
57
|
+
assert_index_equal(data.index, hourly_index_start_utc)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.remote_data
|
|
61
|
+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
|
|
62
|
+
def test_get_solargis_http_error():
|
|
63
|
+
# Test if HTTPError is raised if date outside range is specified
|
|
64
|
+
with pytest.raises(requests.HTTPError, match="data coverage"):
|
|
65
|
+
_, _ = pvlib.iotools.get_solargis(
|
|
66
|
+
latitude=48.61259, longitude=20.827079,
|
|
67
|
+
start='1920-01-01', end='1920-01-01', # date outside range
|
|
68
|
+
variables=['GHI', 'DNI'], time_resolution='HOURLY', api_key='demo')
|
|
@@ -174,9 +174,9 @@ def test_get_solcast_tmy(
|
|
|
174
174
|
),
|
|
175
175
|
pd.DataFrame(
|
|
176
176
|
[[9.4200e+02, 8.4300e+02, 1.0174e+05, 3.0000e+01, 7.8000e+00,
|
|
177
|
-
3.1600e+02, 1.0100e+03, 2.0000e+00, 4.6000e+00, 1.6400e+
|
|
177
|
+
3.1600e+02, 1.0100e+03, 2.0000e+00, 4.6000e+00, 1.6400e+00, 90],
|
|
178
178
|
[9.3600e+02, 8.3200e+02, 1.0179e+05, 3.0000e+01, 7.9000e+00,
|
|
179
|
-
3.1600e+02, 9.9600e+02, 1.4000e+01, 4.5000e+00, 1.6300e+
|
|
179
|
+
3.1600e+02, 9.9600e+02, 1.4000e+01, 4.5000e+00, 1.6300e+00, 0]],
|
|
180
180
|
columns=[
|
|
181
181
|
'dni', 'ghi', 'pressure', 'temp_air', 'wind_speed',
|
|
182
182
|
'wind_direction', 'poa_global', 'solar_azimuth',
|
|
@@ -5,12 +5,13 @@ from numpy import nan
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from pvlib.iotools import solrad
|
|
8
|
-
from ..conftest import DATA_DIR, assert_frame_equal
|
|
8
|
+
from ..conftest import DATA_DIR, assert_frame_equal, RERUNS, RERUNS_DELAY
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
testfile = DATA_DIR / 'abq19056.dat'
|
|
12
12
|
testfile_mad = DATA_DIR / 'msn19056.dat'
|
|
13
|
-
|
|
13
|
+
https_testfile = ('https://gml.noaa.gov/aftp/data/radiation/solrad/msn/'
|
|
14
|
+
'2019/msn19056.dat')
|
|
14
15
|
|
|
15
16
|
columns = [
|
|
16
17
|
'year', 'julian_day', 'month', 'day', 'hour', 'minute', 'decimal_time',
|
|
@@ -87,15 +88,65 @@ dtypes_mad = [
|
|
|
87
88
|
'int64', 'float64', 'int64', 'float64', 'int64', 'float64', 'int64',
|
|
88
89
|
'float64', 'int64', 'float64', 'float64', 'float64', 'float64', 'float64',
|
|
89
90
|
'float64', 'float64']
|
|
91
|
+
meta = {'station_name': 'Albuquerque', 'latitude': 35.03796,
|
|
92
|
+
'longitude': -106.62211, 'altitude': 1617, 'TZ': -7}
|
|
93
|
+
meta_mad = {'station_name': 'Madison', 'latitude': 43.07250,
|
|
94
|
+
'longitude': -89.41133, 'altitude': 271, 'TZ': -6}
|
|
90
95
|
|
|
91
96
|
|
|
92
|
-
@pytest.mark.parametrize('testfile,index,columns,values,dtypes', [
|
|
93
|
-
(testfile, index, columns, values, dtypes),
|
|
94
|
-
(testfile_mad, index, columns_mad, values_mad, dtypes_mad)
|
|
97
|
+
@pytest.mark.parametrize('testfile,index,columns,values,dtypes,meta', [
|
|
98
|
+
(testfile, index, columns, values, dtypes, meta),
|
|
99
|
+
(testfile_mad, index, columns_mad, values_mad, dtypes_mad, meta_mad)
|
|
95
100
|
])
|
|
96
|
-
def test_read_solrad(testfile, index, columns, values, dtypes):
|
|
101
|
+
def test_read_solrad(testfile, index, columns, values, dtypes, meta):
|
|
97
102
|
expected = pd.DataFrame(values, columns=columns, index=index)
|
|
98
103
|
for (col, _dtype) in zip(expected.columns, dtypes):
|
|
99
104
|
expected[col] = expected[col].astype(_dtype)
|
|
100
|
-
out = solrad.read_solrad(testfile)
|
|
105
|
+
out, m = solrad.read_solrad(testfile)
|
|
101
106
|
assert_frame_equal(out, expected)
|
|
107
|
+
assert m == meta
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.remote_data
|
|
111
|
+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
|
|
112
|
+
def test_read_solrad_https():
|
|
113
|
+
# Test reading of https files.
|
|
114
|
+
# If this test begins failing, SOLRAD's data structure or data
|
|
115
|
+
# archive may have changed.
|
|
116
|
+
local_data, _ = solrad.read_solrad(testfile_mad)
|
|
117
|
+
remote_data, _ = solrad.read_solrad(https_testfile)
|
|
118
|
+
# local file only contains four rows to save space
|
|
119
|
+
assert_frame_equal(local_data, remote_data.iloc[:4])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.remote_data
|
|
123
|
+
@pytest.mark.parametrize('testfile, station', [
|
|
124
|
+
(testfile, 'abq'),
|
|
125
|
+
(testfile_mad, 'msn'),
|
|
126
|
+
])
|
|
127
|
+
def test_get_solrad(testfile, station):
|
|
128
|
+
df, meta = solrad.get_solrad(station, "2019-02-25", "2019-02-25")
|
|
129
|
+
|
|
130
|
+
assert meta['station'] == station
|
|
131
|
+
assert isinstance(meta['filenames'], list)
|
|
132
|
+
|
|
133
|
+
assert len(df) == 1440
|
|
134
|
+
assert df.index[0] == pd.to_datetime('2019-02-25 00:00+00:00')
|
|
135
|
+
assert df.index[-1] == pd.to_datetime('2019-02-25 23:59+00:00')
|
|
136
|
+
|
|
137
|
+
expected, _ = solrad.read_solrad(testfile)
|
|
138
|
+
actual = df.reindex(expected.index)
|
|
139
|
+
# ABQ test file has an unexplained NaN in row 4; just verify first 3 rows
|
|
140
|
+
assert_frame_equal(actual.iloc[:3], expected.iloc[:3])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.remote_data
|
|
144
|
+
def test_get_solrad_missing_day():
|
|
145
|
+
# data availability begins for ABQ on 2002-02-01 (DOY 32), so requesting
|
|
146
|
+
# data before that will raise a warning
|
|
147
|
+
message = 'The following file was not found: abq/2002/abq02031.dat'
|
|
148
|
+
with pytest.warns(UserWarning, match=message):
|
|
149
|
+
df, meta = solrad.get_solrad('abq', '2002-01-31', '2002-02-01')
|
|
150
|
+
|
|
151
|
+
# but the data for 2022-02-01 is still returned
|
|
152
|
+
assert not df.empty
|