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.
Files changed (53) hide show
  1. pvlib/__init__.py +1 -0
  2. pvlib/bifacial/utils.py +2 -1
  3. pvlib/clearsky.py +7 -8
  4. pvlib/iam.py +3 -3
  5. pvlib/inverter.py +3 -3
  6. pvlib/iotools/__init__.py +2 -0
  7. pvlib/iotools/solargis.py +214 -0
  8. pvlib/iotools/solcast.py +2 -7
  9. pvlib/iotools/solrad.py +121 -23
  10. pvlib/iotools/srml.py +12 -12
  11. pvlib/iotools/surfrad.py +2 -2
  12. pvlib/irradiance.py +28 -22
  13. pvlib/location.py +3 -1
  14. pvlib/modelchain.py +10 -9
  15. pvlib/pvarray.py +127 -0
  16. pvlib/pvsystem.py +52 -43
  17. pvlib/scaling.py +4 -2
  18. pvlib/shading.py +110 -0
  19. pvlib/singlediode.py +37 -9
  20. pvlib/snow.py +3 -1
  21. pvlib/solarposition.py +38 -30
  22. pvlib/spa.py +3 -11
  23. pvlib/spectrum/mismatch.py +2 -1
  24. pvlib/temperature.py +3 -2
  25. pvlib/tests/bifacial/test_utils.py +6 -5
  26. pvlib/tests/conftest.py +13 -14
  27. pvlib/tests/iotools/test_sodapro.py +2 -1
  28. pvlib/tests/iotools/test_solargis.py +68 -0
  29. pvlib/tests/iotools/test_solcast.py +2 -2
  30. pvlib/tests/iotools/test_solrad.py +58 -7
  31. pvlib/tests/iotools/test_srml.py +7 -14
  32. pvlib/tests/test_clearsky.py +1 -1
  33. pvlib/tests/test_irradiance.py +24 -8
  34. pvlib/tests/test_location.py +1 -1
  35. pvlib/tests/test_modelchain.py +37 -26
  36. pvlib/tests/test_pvarray.py +25 -0
  37. pvlib/tests/test_pvsystem.py +76 -104
  38. pvlib/tests/test_shading.py +130 -11
  39. pvlib/tests/test_singlediode.py +68 -10
  40. pvlib/tests/test_snow.py +1 -1
  41. pvlib/tests/test_solarposition.py +121 -7
  42. pvlib/tests/test_spa.py +5 -15
  43. pvlib/tests/test_temperature.py +4 -4
  44. pvlib/tests/test_tracking.py +2 -2
  45. pvlib/tracking.py +8 -38
  46. pvlib/version.py +1 -5
  47. {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/METADATA +9 -33
  48. {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/RECORD +52 -51
  49. {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/WHEEL +1 -1
  50. pvlib/spa_c_files/SPA_NOTICE.md +0 -39
  51. {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/AUTHORS.md +0 -0
  52. {pvlib-0.10.3.dist-info → pvlib-0.10.5.dist-info}/LICENSE +0 -0
  53. {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.Inf, breakdown_factor=0., breakdown_voltage=-5.5,
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.Inf, breakdown_factor=0.,
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(voc_est, voltage, *args)
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.Inf, breakdown_factor=0.,
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(voc_est, current, *args)
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(voc_est, (current, *args), method_kwargs)
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.Inf,
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(voc_est, *args)
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(voc_est, args, method_kwargs)
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 [1]_.
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: http://rredc.nrel.gov/solar/codesandalgorithms/spa/
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] USNO delta T:
190
- http://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term
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
- http://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term
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 = np.array(time.view(np.int64)/10**9)
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 = np.array(utcday.view(np.int64)/10**9)
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 = np.array(time.view(np.int64)/10**9)
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: Sun's Position
1331
- <http://www.pveducation.org/pvcdrom/2-properties-sunlight/
1332
- suns-position>`_
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
- hrs_minus_tzs = 1 / NS_PER_HR * (
1384
- 2 * times.view(np.int64) - times.normalize().view(np.int64) -
1385
- naive_times.view(np.int64))
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
- naive_times = times.tz_localize(None) # naive but still localized
1394
- tzs = 1 / NS_PER_HR * (
1395
- naive_times.view(np.int64) - times.view(np.int64))
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
- (naive_times.normalize().view(np.int64) +
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 = 1 / NS_PER_HR * (
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, __version__
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
- major, minor = __version__.split('.')[:2]
32
- if int(major + minor) >= 17:
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
@@ -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 np.trapz(e, x=e.T.index, axis=-1)
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 6 Help", Files.pvsyst.com, 2018. [Online]. Available:
366
- http://files.pvsyst.com/help/index.html. [Accessed: 10- Dec- 2018].
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 = np.trapz(vfs_gnd_sky, pts, axis=0)
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 = np.trapz(y, x) / (fx1[1] - fx0[1])
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 = np.trapz(y, x) / (1 - 0)
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 = np.trapz(y, x) / (fx1[1] - fx0[1])
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 = np.trapz(y, x) / (1 - 0)
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
- def has_numba():
143
- try:
144
- import numba
145
- except ImportError:
146
- return False
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(), reason="requires 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='1T', tz='UTC')
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+02, 90],
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+02, 0]],
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