pvlib 0.9.5__py3-none-any.whl → 0.10.0__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 (71) hide show
  1. pvlib/__init__.py +3 -2
  2. pvlib/atmosphere.py +6 -171
  3. pvlib/bifacial/infinite_sheds.py +30 -267
  4. pvlib/bifacial/utils.py +225 -5
  5. pvlib/data/test_psm3_2017.csv +17521 -17521
  6. pvlib/data/test_read_psm3.csv +17522 -17522
  7. pvlib/data/test_read_pvgis_horizon.csv +49 -0
  8. pvlib/data/variables_style_rules.csv +3 -0
  9. pvlib/iam.py +17 -4
  10. pvlib/inverter.py +6 -1
  11. pvlib/iotools/__init__.py +7 -2
  12. pvlib/iotools/acis.py +516 -0
  13. pvlib/iotools/midc.py +4 -4
  14. pvlib/iotools/psm3.py +32 -31
  15. pvlib/iotools/pvgis.py +84 -28
  16. pvlib/iotools/sodapro.py +8 -6
  17. pvlib/iotools/srml.py +121 -18
  18. pvlib/iotools/surfrad.py +2 -2
  19. pvlib/iotools/tmy.py +146 -102
  20. pvlib/irradiance.py +151 -0
  21. pvlib/ivtools/sde.py +11 -7
  22. pvlib/ivtools/sdm.py +16 -10
  23. pvlib/ivtools/utils.py +6 -6
  24. pvlib/location.py +3 -2
  25. pvlib/modelchain.py +67 -70
  26. pvlib/pvsystem.py +160 -532
  27. pvlib/shading.py +41 -0
  28. pvlib/singlediode.py +215 -65
  29. pvlib/soiling.py +3 -3
  30. pvlib/spa.py +327 -368
  31. pvlib/spectrum/__init__.py +8 -2
  32. pvlib/spectrum/mismatch.py +335 -0
  33. pvlib/temperature.py +1 -8
  34. pvlib/tests/bifacial/test_infinite_sheds.py +0 -111
  35. pvlib/tests/bifacial/test_utils.py +101 -4
  36. pvlib/tests/conftest.py +0 -31
  37. pvlib/tests/iotools/test_acis.py +213 -0
  38. pvlib/tests/iotools/test_midc.py +6 -6
  39. pvlib/tests/iotools/test_psm3.py +3 -3
  40. pvlib/tests/iotools/test_pvgis.py +21 -14
  41. pvlib/tests/iotools/test_sodapro.py +1 -1
  42. pvlib/tests/iotools/test_srml.py +71 -6
  43. pvlib/tests/iotools/test_tmy.py +43 -8
  44. pvlib/tests/ivtools/test_sde.py +19 -17
  45. pvlib/tests/ivtools/test_sdm.py +9 -4
  46. pvlib/tests/test_atmosphere.py +6 -62
  47. pvlib/tests/test_iam.py +12 -0
  48. pvlib/tests/test_irradiance.py +40 -2
  49. pvlib/tests/test_location.py +1 -1
  50. pvlib/tests/test_modelchain.py +33 -76
  51. pvlib/tests/test_pvsystem.py +366 -201
  52. pvlib/tests/test_shading.py +28 -0
  53. pvlib/tests/test_singlediode.py +166 -30
  54. pvlib/tests/test_soiling.py +8 -7
  55. pvlib/tests/test_spa.py +6 -7
  56. pvlib/tests/test_spectrum.py +145 -1
  57. pvlib/tests/test_temperature.py +0 -7
  58. pvlib/tests/test_tools.py +25 -0
  59. pvlib/tests/test_tracking.py +0 -149
  60. pvlib/tools.py +26 -1
  61. pvlib/tracking.py +1 -269
  62. {pvlib-0.9.5.dist-info → pvlib-0.10.0.dist-info}/METADATA +1 -9
  63. {pvlib-0.9.5.dist-info → pvlib-0.10.0.dist-info}/RECORD +67 -68
  64. pvlib/forecast.py +0 -1211
  65. pvlib/iotools/ecmwf_macc.py +0 -312
  66. pvlib/tests/iotools/test_ecmwf_macc.py +0 -162
  67. pvlib/tests/test_forecast.py +0 -228
  68. {pvlib-0.9.5.dist-info → pvlib-0.10.0.dist-info}/AUTHORS.md +0 -0
  69. {pvlib-0.9.5.dist-info → pvlib-0.10.0.dist-info}/LICENSE +0 -0
  70. {pvlib-0.9.5.dist-info → pvlib-0.10.0.dist-info}/WHEEL +0 -0
  71. {pvlib-0.9.5.dist-info → pvlib-0.10.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,9 @@
1
1
  from pvlib.spectrum.spectrl2 import spectrl2 # noqa: F401
2
- from pvlib.spectrum.mismatch import (get_example_spectral_response, get_am15g,
3
- calc_spectral_mismatch_field)
2
+ from pvlib.spectrum.mismatch import ( # noqa: F401
3
+ calc_spectral_mismatch_field,
4
+ get_am15g,
5
+ get_example_spectral_response,
6
+ spectral_factor_caballero,
7
+ spectral_factor_firstsolar,
8
+ spectral_factor_sapm,
9
+ )
@@ -8,6 +8,8 @@ import pandas as pd
8
8
  from scipy.interpolate import interp1d
9
9
  import os
10
10
 
11
+ from warnings import warn
12
+
11
13
 
12
14
  def get_example_spectral_response(wavelength=None):
13
15
  '''
@@ -235,3 +237,336 @@ def calc_spectral_mismatch_field(sr, e_sun, e_ref=None):
235
237
  smm = pd.Series(smm, index=e_sun.index)
236
238
 
237
239
  return smm
240
+
241
+
242
+ def spectral_factor_firstsolar(precipitable_water, airmass_absolute,
243
+ module_type=None, coefficients=None,
244
+ min_precipitable_water=0.1,
245
+ max_precipitable_water=8):
246
+ r"""
247
+ Spectral mismatch modifier based on precipitable water and absolute
248
+ (pressure-adjusted) airmass.
249
+
250
+ Estimates a spectral mismatch modifier :math:`M` representing the effect on
251
+ module short circuit current of variation in the spectral
252
+ irradiance. :math:`M` is estimated from absolute (pressure currected) air
253
+ mass, :math:`AM_a`, and precipitable water, :math:`Pw`, using the following
254
+ function:
255
+
256
+ .. math::
257
+
258
+ M = c_1 + c_2 AM_a + c_3 Pw + c_4 AM_a^{0.5}
259
+ + c_5 Pw^{0.5} + c_6 \frac{AM_a} {Pw^{0.5}}
260
+
261
+ Default coefficients are determined for several cell types with
262
+ known quantum efficiency curves, by using the Simple Model of the
263
+ Atmospheric Radiative Transfer of Sunshine (SMARTS) [1]_. Using
264
+ SMARTS, spectrums are simulated with all combinations of AMa and
265
+ Pw where:
266
+
267
+ * :math:`0.5 \textrm{cm} <= Pw <= 5 \textrm{cm}`
268
+ * :math:`1.0 <= AM_a <= 5.0`
269
+ * Spectral range is limited to that of CMP11 (280 nm to 2800 nm)
270
+ * spectrum simulated on a plane normal to the sun
271
+ * All other parameters fixed at G173 standard
272
+
273
+ From these simulated spectra, M is calculated using the known
274
+ quantum efficiency curves. Multiple linear regression is then
275
+ applied to fit Eq. 1 to determine the coefficients for each module.
276
+
277
+ Based on the PVLIB Matlab function ``pvl_FSspeccorr`` by Mitchell
278
+ Lee and Alex Panchula of First Solar, 2016 [2]_.
279
+
280
+ Parameters
281
+ ----------
282
+ precipitable_water : numeric
283
+ atmospheric precipitable water. [cm]
284
+
285
+ airmass_absolute : numeric
286
+ absolute (pressure-adjusted) airmass. [unitless]
287
+
288
+ module_type : str, optional
289
+ a string specifying a cell type. Values of 'cdte', 'monosi', 'xsi',
290
+ 'multisi', and 'polysi' (can be lower or upper case). If provided,
291
+ module_type selects default coefficients for the following modules:
292
+
293
+ * 'cdte' - First Solar Series 4-2 CdTe module.
294
+ * 'monosi', 'xsi' - First Solar TetraSun module.
295
+ * 'multisi', 'polysi' - anonymous multi-crystalline silicon module.
296
+ * 'cigs' - anonymous copper indium gallium selenide module.
297
+ * 'asi' - anonymous amorphous silicon module.
298
+
299
+ The module used to calculate the spectral correction
300
+ coefficients corresponds to the Multi-crystalline silicon
301
+ Manufacturer 2 Model C from [3]_. The spectral response (SR) of CIGS
302
+ and a-Si modules used to derive coefficients can be found in [4]_
303
+
304
+ coefficients : array-like, optional
305
+ Allows for entry of user-defined spectral correction
306
+ coefficients. Coefficients must be of length 6. Derivation of
307
+ coefficients requires use of SMARTS and PV module quantum
308
+ efficiency curve. Useful for modeling PV module types which are
309
+ not included as defaults, or to fine tune the spectral
310
+ correction to a particular PV module. Note that the parameters for
311
+ modules with very similar quantum efficiency should be similar,
312
+ in most cases limiting the need for module specific coefficients.
313
+
314
+ min_precipitable_water : float, default 0.1
315
+ minimum atmospheric precipitable water. Any ``precipitable_water``
316
+ value lower than ``min_precipitable_water``
317
+ is set to ``min_precipitable_water`` to avoid model divergence. [cm]
318
+
319
+ max_precipitable_water : float, default 8
320
+ maximum atmospheric precipitable water. Any ``precipitable_water``
321
+ value greater than ``max_precipitable_water``
322
+ is set to ``np.nan`` to avoid model divergence. [cm]
323
+
324
+ Returns
325
+ -------
326
+ modifier: array-like
327
+ spectral mismatch factor (unitless) which can be multiplied
328
+ with broadband irradiance reaching a module's cells to estimate
329
+ effective irradiance, i.e., the irradiance that is converted to
330
+ electrical current.
331
+
332
+ References
333
+ ----------
334
+ .. [1] Gueymard, Christian. SMARTS2: a simple model of the atmospheric
335
+ radiative transfer of sunshine: algorithms and performance
336
+ assessment. Cocoa, FL: Florida Solar Energy Center, 1995.
337
+ .. [2] Lee, Mitchell, and Panchula, Alex. "Spectral Correction for
338
+ Photovoltaic Module Performance Based on Air Mass and Precipitable
339
+ Water." IEEE Photovoltaic Specialists Conference, Portland, 2016
340
+ .. [3] Marion, William F., et al. User's Manual for Data for Validating
341
+ Models for PV Module Performance. National Renewable Energy
342
+ Laboratory, 2014. http://www.nrel.gov/docs/fy14osti/61610.pdf
343
+ .. [4] Schweiger, M. and Hermann, W, Influence of Spectral Effects
344
+ on Energy Yield of Different PV Modules: Comparison of Pwat and
345
+ MMF Approach, TUV Rheinland Energy GmbH report 21237296.003,
346
+ January 2017
347
+ """
348
+
349
+ # --- Screen Input Data ---
350
+
351
+ # *** Pw ***
352
+ # Replace Pw Values below 0.1 cm with 0.1 cm to prevent model from
353
+ # diverging"
354
+ pw = np.atleast_1d(precipitable_water)
355
+ pw = pw.astype('float64')
356
+ if np.min(pw) < min_precipitable_water:
357
+ pw = np.maximum(pw, min_precipitable_water)
358
+ warn('Exceptionally low pw values replaced with '
359
+ f'{min_precipitable_water} cm to prevent model divergence')
360
+
361
+ # Warn user about Pw data that is exceptionally high
362
+ if np.max(pw) > max_precipitable_water:
363
+ pw[pw > max_precipitable_water] = np.nan
364
+ warn('Exceptionally high pw values replaced by np.nan: '
365
+ 'check input data.')
366
+
367
+ # *** AMa ***
368
+ # Replace Extremely High AM with AM 10 to prevent model divergence
369
+ # AM > 10 will only occur very close to sunset
370
+ if np.max(airmass_absolute) > 10:
371
+ airmass_absolute = np.minimum(airmass_absolute, 10)
372
+
373
+ # Warn user about AMa data that is exceptionally low
374
+ if np.min(airmass_absolute) < 0.58:
375
+ warn('Exceptionally low air mass: ' +
376
+ 'model not intended for extra-terrestrial use')
377
+ # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of
378
+ # Mina Pirquita, Argentian = 4340 m. Highest elevation city with
379
+ # population over 50,000.
380
+
381
+ _coefficients = {}
382
+ _coefficients['cdte'] = (
383
+ 0.86273, -0.038948, -0.012506, 0.098871, 0.084658, -0.0042948)
384
+ _coefficients['monosi'] = (
385
+ 0.85914, -0.020880, -0.0058853, 0.12029, 0.026814, -0.0017810)
386
+ _coefficients['xsi'] = _coefficients['monosi']
387
+ _coefficients['polysi'] = (
388
+ 0.84090, -0.027539, -0.0079224, 0.13570, 0.038024, -0.0021218)
389
+ _coefficients['multisi'] = _coefficients['polysi']
390
+ _coefficients['cigs'] = (
391
+ 0.85252, -0.022314, -0.0047216, 0.13666, 0.013342, -0.0008945)
392
+ _coefficients['asi'] = (
393
+ 1.12094, -0.047620, -0.0083627, -0.10443, 0.098382, -0.0033818)
394
+
395
+ if module_type is not None and coefficients is None:
396
+ coefficients = _coefficients[module_type.lower()]
397
+ elif module_type is None and coefficients is not None:
398
+ pass
399
+ elif module_type is None and coefficients is None:
400
+ raise TypeError('No valid input provided, both module_type and ' +
401
+ 'coefficients are None')
402
+ else:
403
+ raise TypeError('Cannot resolve input, must supply only one of ' +
404
+ 'module_type and coefficients')
405
+
406
+ # Evaluate Spectral Shift
407
+ coeff = coefficients
408
+ ama = airmass_absolute
409
+ modifier = (
410
+ coeff[0] + coeff[1]*ama + coeff[2]*pw + coeff[3]*np.sqrt(ama) +
411
+ coeff[4]*np.sqrt(pw) + coeff[5]*ama/np.sqrt(pw))
412
+
413
+ return modifier
414
+
415
+
416
+ def spectral_factor_sapm(airmass_absolute, module):
417
+ """
418
+ Calculates the SAPM spectral loss coefficient, F1.
419
+
420
+ Parameters
421
+ ----------
422
+ airmass_absolute : numeric
423
+ Absolute airmass
424
+
425
+ module : dict-like
426
+ A dict, Series, or DataFrame defining the SAPM performance
427
+ parameters. See the :py:func:`sapm` notes section for more
428
+ details.
429
+
430
+ Returns
431
+ -------
432
+ F1 : numeric
433
+ The SAPM spectral loss coefficient.
434
+
435
+ Notes
436
+ -----
437
+ nan airmass values will result in 0 output.
438
+ """
439
+
440
+ am_coeff = [module['A4'], module['A3'], module['A2'], module['A1'],
441
+ module['A0']]
442
+
443
+ spectral_loss = np.polyval(am_coeff, airmass_absolute)
444
+
445
+ spectral_loss = np.where(np.isnan(spectral_loss), 0, spectral_loss)
446
+
447
+ spectral_loss = np.maximum(0, spectral_loss)
448
+
449
+ if isinstance(airmass_absolute, pd.Series):
450
+ spectral_loss = pd.Series(spectral_loss, airmass_absolute.index)
451
+
452
+ return spectral_loss
453
+
454
+
455
+ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500,
456
+ module_type=None, coefficients=None):
457
+ r"""
458
+ Estimate a technology-specific spectral mismatch modifier from
459
+ airmass, aerosol optical depth, and atmospheric precipitable water,
460
+ using the Caballero model.
461
+
462
+ The model structure was motivated by examining the effect of these three
463
+ atmospheric parameters on simulated irradiance spectra and spectral
464
+ modifiers. However, the coefficient values reported in [1]_ and
465
+ available here via the ``module_type`` parameter were determined
466
+ by fitting the model equations to spectral factors calculated from
467
+ global tilted spectral irradiance measurements taken in the city of
468
+ Jaén, Spain. See [1]_ for details.
469
+
470
+ Parameters
471
+ ----------
472
+ precipitable_water : numeric
473
+ atmospheric precipitable water. [cm]
474
+
475
+ airmass_absolute : numeric
476
+ absolute (pressure-adjusted) airmass. [unitless]
477
+
478
+ aod500 : numeric
479
+ atmospheric aerosol optical depth at 500 nm. [unitless]
480
+
481
+ module_type : str, optional
482
+ One of the following PV technology strings from [1]_:
483
+
484
+ * ``'cdte'`` - anonymous CdTe module.
485
+ * ``'monosi'``, - anonymous sc-si module.
486
+ * ``'multisi'``, - anonymous mc-si- module.
487
+ * ``'cigs'`` - anonymous copper indium gallium selenide module.
488
+ * ``'asi'`` - anonymous amorphous silicon module.
489
+ * ``'perovskite'`` - anonymous pervoskite module.
490
+
491
+ coefficients : array-like, optional
492
+ user-defined coefficients, if not using one of the default coefficient
493
+ sets via the ``module_type`` parameter.
494
+
495
+ Returns
496
+ -------
497
+ modifier: numeric
498
+ spectral mismatch factor (unitless) which is multiplied
499
+ with broadband irradiance reaching a module's cells to estimate
500
+ effective irradiance, i.e., the irradiance that is converted to
501
+ electrical current.
502
+
503
+ References
504
+ ----------
505
+ .. [1] Caballero, J.A., Fernández, E., Theristis, M.,
506
+ Almonacid, F., and Nofuentes, G. "Spectral Corrections Based on
507
+ Air Mass, Aerosol Optical Depth and Precipitable Water
508
+ for PV Performance Modeling."
509
+ IEEE Journal of Photovoltaics 2018, 8(2), 552-558.
510
+ :doi:`10.1109/jphotov.2017.2787019`
511
+ """
512
+
513
+ if module_type is None and coefficients is None:
514
+ raise ValueError('Must provide either `module_type` or `coefficients`')
515
+ if module_type is not None and coefficients is not None:
516
+ raise ValueError('Only one of `module_type` and `coefficients` should '
517
+ 'be provided')
518
+
519
+ # Experimental coefficients from [1]_.
520
+ # The extra 0/1 coefficients at the end are used to enable/disable
521
+ # terms to match the different equation forms in Table 1.
522
+ _coefficients = {}
523
+ _coefficients['cdte'] = (
524
+ 1.0044, 0.0095, -0.0037, 0.0002, 0.0000, -0.0046,
525
+ -0.0182, 0, 0.0095, 0.0068, 0, 1)
526
+ _coefficients['monosi'] = (
527
+ 0.9706, 0.0377, -0.0123, 0.0025, -0.0002, 0.0159,
528
+ -0.0165, 0, -0.0016, -0.0027, 1, 0)
529
+ _coefficients['multisi'] = (
530
+ 0.9836, 0.0254, -0.0085, 0.0016, -0.0001, 0.0094,
531
+ -0.0132, 0, -0.0002, -0.0011, 1, 0)
532
+ _coefficients['cigs'] = (
533
+ 0.9801, 0.0283, -0.0092, 0.0019, -0.0001, 0.0117,
534
+ -0.0126, 0, -0.0011, -0.0019, 1, 0)
535
+ _coefficients['asi'] = (
536
+ 1.1060, -0.0848, 0.0302, -0.0076, 0.0006, -0.1283,
537
+ 0.0986, -0.0254, 0.0156, 0.0146, 1, 0)
538
+ _coefficients['perovskite'] = (
539
+ 1.0637, -0.0491, 0.0180, -0.0047, 0.0004, -0.0773,
540
+ 0.0583, -0.0159, 0.01251, 0.0109, 1, 0)
541
+
542
+ if module_type is not None:
543
+ coeff = _coefficients[module_type]
544
+ else:
545
+ coeff = coefficients
546
+
547
+ # Evaluate spectral correction factor
548
+ ama = airmass_absolute
549
+ aod500_ref = 0.084
550
+ pw_ref = 1.4164
551
+
552
+ f_AM = (
553
+ coeff[0]
554
+ + coeff[1] * ama
555
+ + coeff[2] * ama**2
556
+ + coeff[3] * ama**3
557
+ + coeff[4] * ama**4
558
+ )
559
+ # Eq 6, with Table 1
560
+ f_AOD = (aod500 - aod500_ref) * (
561
+ coeff[5]
562
+ + coeff[10] * coeff[6] * ama
563
+ + coeff[11] * coeff[6] * np.log(ama)
564
+ + coeff[7] * ama**2
565
+ )
566
+ # Eq 7, with Table 1
567
+ f_PW = (precipitable_water - pw_ref) * (
568
+ coeff[8]
569
+ + coeff[9] * np.log(ama)
570
+ )
571
+ modifier = f_AM + f_AOD + f_PW # Eq 5
572
+ return modifier
pvlib/temperature.py CHANGED
@@ -291,7 +291,7 @@ def sapm_cell_from_module(module_temperature, poa_global, deltaT,
291
291
 
292
292
 
293
293
  def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
294
- eta_m=None, module_efficiency=0.1, alpha_absorption=0.9):
294
+ module_efficiency=0.1, alpha_absorption=0.9):
295
295
  r"""
296
296
  Calculate cell temperature using an empirical heat loss factor model
297
297
  as implemented in PVsyst.
@@ -321,8 +321,6 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
321
321
  in :eq:`pvsyst`.
322
322
  :math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`
323
323
 
324
- eta_m : numeric, default None (deprecated, use module_efficiency instead)
325
-
326
324
  module_efficiency : numeric, default 0.1
327
325
  Module external efficiency as a fraction. Parameter :math:`\eta_{m}`
328
326
  in :eq:`pvsyst`. Calculate as
@@ -378,11 +376,6 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
378
376
  37.93103448275862
379
377
  """ # noQA: E501
380
378
 
381
- if eta_m:
382
- warn_deprecated(
383
- since='v0.9', message='eta_m overwriting module_efficiency',
384
- name='eta_m', alternative='module_efficiency', removal='v0.10')
385
- module_efficiency = eta_m
386
379
  total_loss_factor = u_c + u_v * wind_speed
387
380
  heat_input = poa_global * alpha_absorption * (1 - module_efficiency)
388
381
  temp_difference = heat_input / total_loss_factor
@@ -5,7 +5,6 @@ test infinite sheds
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  from pvlib.bifacial import infinite_sheds
8
- from pvlib.tools import cosd
9
8
  from ..conftest import assert_series_equal
10
9
 
11
10
  import pytest
@@ -42,116 +41,6 @@ def test_system():
42
41
  return syst, pts, vfs_ground_sky
43
42
 
44
43
 
45
- @pytest.mark.parametrize("vectorize", [True, False])
46
- def test__vf_ground_sky_integ(test_system, vectorize):
47
- ts, pts, vfs_gnd_sky = test_system
48
- # pass rotation here since max_rows=1 for the hand-solved case in
49
- # the fixture test_system, which means the ground-to-sky view factor
50
- # isn't summed over enough rows for symmetry to hold.
51
- vf_integ = infinite_sheds._vf_ground_sky_integ(
52
- ts['rotation'], ts['surface_azimuth'],
53
- ts['gcr'], ts['height'], ts['pitch'],
54
- max_rows=1, npoints=3, vectorize=vectorize)
55
- expected_vf_integ = np.trapz(vfs_gnd_sky, pts)
56
- assert np.isclose(vf_integ, expected_vf_integ, rtol=0.1)
57
-
58
-
59
- def test__vf_row_sky_integ(test_system):
60
- ts, _, _ = test_system
61
- gcr = ts['gcr']
62
- surface_tilt = ts['surface_tilt']
63
- f_x = np.array([0., 0.5, 1.])
64
- shaded = []
65
- noshade = []
66
- for x in f_x:
67
- s, ns = infinite_sheds._vf_row_sky_integ(
68
- x, surface_tilt, gcr, npoints=100)
69
- shaded.append(s)
70
- noshade.append(ns)
71
-
72
- def analytic(gcr, surface_tilt, x):
73
- c = cosd(surface_tilt)
74
- a = 1. / gcr
75
- dx = np.sqrt(a**2 - 2 * a * c * x + x**2)
76
- return - a * (c**2 - 1) * np.arctanh((x - a * c) / dx) - c * dx
77
-
78
- expected_shade = 0.5 * (f_x * cosd(surface_tilt)
79
- - analytic(gcr, surface_tilt, 1 - f_x)
80
- + analytic(gcr, surface_tilt, 1.))
81
- expected_noshade = 0.5 * ((1 - f_x) * cosd(surface_tilt)
82
- + analytic(gcr, surface_tilt, 1. - f_x)
83
- - analytic(gcr, surface_tilt, 0.))
84
- shaded = np.array(shaded)
85
- noshade = np.array(noshade)
86
- assert np.allclose(shaded, expected_shade)
87
- assert np.allclose(noshade, expected_noshade)
88
-
89
-
90
- def test__poa_sky_diffuse_pv():
91
- dhi = np.array([np.nan, 0.0, 500.])
92
- f_x = np.array([0.2, 0.2, 0.5])
93
- vf_shade_sky_integ = np.array([1.0, 0.5, 0.2])
94
- vf_noshade_sky_integ = np.array([0.0, 0.5, 0.8])
95
- poa = infinite_sheds._poa_sky_diffuse_pv(
96
- f_x, dhi, vf_shade_sky_integ, vf_noshade_sky_integ)
97
- expected_poa = np.array([np.nan, 0.0, 500 * (0.5 * 0.2 + 0.5 * 0.8)])
98
- assert np.allclose(poa, expected_poa, equal_nan=True)
99
-
100
-
101
- def test__ground_angle(test_system):
102
- ts, _, _ = test_system
103
- x = np.array([0., 0.5, 1.0])
104
- angles = infinite_sheds._ground_angle(
105
- x, ts['surface_tilt'], ts['gcr'])
106
- expected_angles = np.array([0., 5.866738789543952, 9.896090638982903])
107
- assert np.allclose(angles, expected_angles)
108
-
109
-
110
- def test__ground_angle_zero_gcr():
111
- surface_tilt = 30.0
112
- x = np.array([0.0, 0.5, 1.0])
113
- angles = infinite_sheds._ground_angle(x, surface_tilt, 0)
114
- expected_angles = np.array([0, 0, 0])
115
- assert np.allclose(angles, expected_angles)
116
-
117
-
118
- def test__vf_row_ground(test_system):
119
- ts, _, _ = test_system
120
- x = np.array([0., 0.5, 1.0])
121
- sqr3 = np.sqrt(3)
122
- vfs = infinite_sheds._vf_row_ground(
123
- x, ts['surface_tilt'], ts['gcr'])
124
- expected_vfs = np.array([
125
- 0.5 * (1. - sqr3 / 2),
126
- 0.5 * ((4 + sqr3 / 2) / np.sqrt(17 + 4 * sqr3) - sqr3 / 2),
127
- 0.5 * ((4 + sqr3) / np.sqrt(20 + 8 * sqr3) - sqr3 / 2)])
128
- assert np.allclose(vfs, expected_vfs)
129
-
130
-
131
- def test__vf_row_ground_integ(test_system):
132
- ts, _, _ = test_system
133
- gcr = ts['gcr']
134
- surface_tilt = ts['surface_tilt']
135
- f_x = np.array([0., 0.5, 1.0])
136
- shaded, noshade = infinite_sheds._vf_row_ground_integ(
137
- f_x, surface_tilt, gcr)
138
-
139
- def analytic(x, surface_tilt, gcr):
140
- c = cosd(surface_tilt)
141
- a = 1. / gcr
142
- dx = np.sqrt(a**2 + 2 * a * c * x + x**2)
143
- return c * dx - a * (c**2 - 1) * np.arctanh((a * c + x) / dx)
144
-
145
- expected_shade = 0.5 * (analytic(f_x, surface_tilt, gcr)
146
- - analytic(0., surface_tilt, gcr)
147
- - f_x * cosd(surface_tilt))
148
- expected_noshade = 0.5 * (analytic(1., surface_tilt, gcr)
149
- - analytic(f_x, surface_tilt, gcr)
150
- - (1. - f_x) * cosd(surface_tilt))
151
- assert np.allclose(shaded, expected_shade)
152
- assert np.allclose(noshade, expected_noshade)
153
-
154
-
155
44
  def test__poa_ground_shadows():
156
45
  poa_ground, f_gnd_beam, df, vf_gnd_sky = (300., 0.5, 0.5, 0.2)
157
46
  result = infinite_sheds._poa_ground_shadows(
@@ -4,6 +4,8 @@ test bifical.utils
4
4
  import numpy as np
5
5
  import pytest
6
6
  from pvlib.bifacial import utils
7
+ from pvlib.shading import masking_angle, ground_angle
8
+ from pvlib.tools import cosd
7
9
 
8
10
 
9
11
  @pytest.fixture
@@ -79,10 +81,105 @@ def test__unshaded_ground_fraction(
79
81
  def test__vf_ground_sky_2d(test_system_fixed_tilt):
80
82
  # vector input
81
83
  ts, pts, vfs_gnd_sky = test_system_fixed_tilt
82
- vfs = utils._vf_ground_sky_2d(pts, ts['rotation'], ts['gcr'],
83
- ts['pitch'], ts['height'], max_rows=1)
84
+ vfs = utils.vf_ground_sky_2d(ts['rotation'], ts['gcr'], pts,
85
+ ts['pitch'], ts['height'], max_rows=1)
84
86
  assert np.allclose(vfs, vfs_gnd_sky, rtol=0.1) # middle point vf is off
85
87
  # test with singleton x
86
- vf = utils._vf_ground_sky_2d(pts[0], ts['rotation'], ts['gcr'],
87
- ts['pitch'], ts['height'], max_rows=1)
88
+ vf = utils.vf_ground_sky_2d(ts['rotation'], ts['gcr'], pts[0],
89
+ ts['pitch'], ts['height'], max_rows=1)
88
90
  assert np.isclose(vf, vfs_gnd_sky[0])
91
+
92
+
93
+ @pytest.mark.parametrize("vectorize", [True, False])
94
+ def test_vf_ground_sky_2d_integ(test_system_fixed_tilt, vectorize):
95
+ ts, pts, vfs_gnd_sky = test_system_fixed_tilt
96
+ # pass rotation here since max_rows=1 for the hand-solved case in
97
+ # the fixture test_system, which means the ground-to-sky view factor
98
+ # isn't summed over enough rows for symmetry to hold.
99
+ vf_integ = utils.vf_ground_sky_2d_integ(
100
+ ts['rotation'], ts['gcr'], ts['height'], ts['pitch'],
101
+ max_rows=1, npoints=3, vectorize=vectorize)
102
+ expected_vf_integ = np.trapz(vfs_gnd_sky, pts, axis=0)
103
+ assert np.isclose(vf_integ, expected_vf_integ, rtol=0.1)
104
+
105
+
106
+ def test_vf_row_sky_2d(test_system_fixed_tilt):
107
+ ts, _, _ = test_system_fixed_tilt
108
+ # with float input, fx at top of row
109
+ vf = utils.vf_row_sky_2d(ts['surface_tilt'], ts['gcr'], 1.)
110
+ expected = 0.5 * (1 + cosd(ts['surface_tilt']))
111
+ assert np.isclose(vf, expected)
112
+ # with array input
113
+ fx = np.array([0., 0.5, 1.])
114
+ vf = utils.vf_row_sky_2d(ts['surface_tilt'], ts['gcr'], fx)
115
+ phi = masking_angle(ts['surface_tilt'], ts['gcr'], fx)
116
+ expected = 0.5 * (1 + cosd(ts['surface_tilt'] + phi))
117
+ assert np.allclose(vf, expected)
118
+
119
+
120
+ def test_vf_row_sky_2d_integ(test_system_fixed_tilt):
121
+ ts, _, _ = test_system_fixed_tilt
122
+ # with float input, check end position
123
+ vf = utils.vf_row_sky_2d_integ(ts['surface_tilt'], ts['gcr'], 1., 1.)
124
+ expected = utils.vf_row_sky_2d(ts['surface_tilt'], ts['gcr'], 1.)
125
+ assert np.isclose(vf, expected)
126
+ # with array input
127
+ fx0 = np.array([0., 0.5])
128
+ fx1 = np.array([0., 0.8])
129
+ vf = utils.vf_row_sky_2d_integ(ts['surface_tilt'], ts['gcr'], fx0, fx1)
130
+ phi = masking_angle(ts['surface_tilt'], ts['gcr'], fx0[0])
131
+ y0 = 0.5 * (1 + cosd(ts['surface_tilt'] + phi))
132
+ x = np.arange(fx0[1], fx1[1], 1e-4)
133
+ phi_y = masking_angle(ts['surface_tilt'], ts['gcr'], x)
134
+ y = 0.5 * (1 + cosd(ts['surface_tilt'] + phi_y))
135
+ y1 = np.trapz(y, x) / (fx1[1] - fx0[1])
136
+ expected = np.array([y0, y1])
137
+ assert np.allclose(vf, expected, rtol=1e-3)
138
+ # with defaults (0, 1)
139
+ vf = utils.vf_row_sky_2d_integ(ts['surface_tilt'], ts['gcr'])
140
+ x = np.arange(0, 1, 1e-4)
141
+ phi_y = masking_angle(ts['surface_tilt'], ts['gcr'], x)
142
+ y = 0.5 * (1 + cosd(ts['surface_tilt'] + phi_y))
143
+ y1 = np.trapz(y, x) / (1 - 0)
144
+ assert np.allclose(vf, y1, rtol=1e-3)
145
+
146
+
147
+ def test_vf_row_ground_2d(test_system_fixed_tilt):
148
+ ts, _, _ = test_system_fixed_tilt
149
+ # with float input, fx at bottom of row
150
+ vf = utils.vf_row_ground_2d(ts['surface_tilt'], ts['gcr'], 0.)
151
+ expected = 0.5 * (1. - cosd(ts['surface_tilt']))
152
+ assert np.isclose(vf, expected)
153
+ # with array input
154
+ fx = np.array([0., 0.5, 1.0])
155
+ vf = utils.vf_row_ground_2d(ts['surface_tilt'], ts['gcr'], fx)
156
+ phi = ground_angle(ts['surface_tilt'], ts['gcr'], fx)
157
+ expected = 0.5 * (1 - cosd(phi - ts['surface_tilt']))
158
+ assert np.allclose(vf, expected)
159
+
160
+
161
+ def test_vf_ground_2d_integ(test_system_fixed_tilt):
162
+ ts, _, _ = test_system_fixed_tilt
163
+ # with float input, check end position
164
+ vf = utils.vf_row_ground_2d_integ(ts['surface_tilt'], ts['gcr'], 0., 0.)
165
+ expected = utils.vf_row_ground_2d(ts['surface_tilt'], ts['gcr'], 0.)
166
+ assert np.isclose(vf, expected)
167
+ # with array input
168
+ fx0 = np.array([0., 0.5])
169
+ fx1 = np.array([0., 0.8])
170
+ vf = utils.vf_row_ground_2d_integ(ts['surface_tilt'], ts['gcr'], fx0, fx1)
171
+ phi = ground_angle(ts['surface_tilt'], ts['gcr'], fx0[0])
172
+ y0 = 0.5 * (1 - cosd(phi - ts['surface_tilt']))
173
+ x = np.arange(fx0[1], fx1[1], 1e-4)
174
+ phi_y = ground_angle(ts['surface_tilt'], ts['gcr'], x)
175
+ y = 0.5 * (1 - cosd(phi_y - ts['surface_tilt']))
176
+ y1 = np.trapz(y, x) / (fx1[1] - fx0[1])
177
+ expected = np.array([y0, y1])
178
+ assert np.allclose(vf, expected, rtol=1e-2)
179
+ # with defaults (0, 1)
180
+ vf = utils.vf_row_ground_2d_integ(ts['surface_tilt'], ts['gcr'], 0, 1)
181
+ x = np.arange(0, 1, 1e-4)
182
+ phi_y = ground_angle(ts['surface_tilt'], ts['gcr'], x)
183
+ y = 0.5 * (1 - cosd(phi_y - ts['surface_tilt']))
184
+ y1 = np.trapz(y, x) / (1 - 0)
185
+ assert np.allclose(vf, y1, rtol=1e-2)
pvlib/tests/conftest.py CHANGED
@@ -141,23 +141,6 @@ def has_numba():
141
141
 
142
142
  requires_numba = pytest.mark.skipif(not has_numba(), reason="requires numba")
143
143
 
144
- try:
145
- import siphon
146
- has_siphon = True
147
- except ImportError:
148
- has_siphon = False
149
-
150
- requires_siphon = pytest.mark.skipif(not has_siphon,
151
- reason='requires siphon')
152
-
153
- try:
154
- import netCDF4 # noqa: F401
155
- has_netCDF4 = True
156
- except ImportError:
157
- has_netCDF4 = False
158
-
159
- requires_netCDF4 = pytest.mark.skipif(not has_netCDF4,
160
- reason='requires netCDF4')
161
144
 
162
145
  try:
163
146
  import pvfactors # noqa: F401
@@ -178,20 +161,6 @@ except ImportError:
178
161
  requires_pysam = pytest.mark.skipif(not has_pysam, reason="requires PySAM")
179
162
 
180
163
 
181
- try:
182
- import cftime # noqa: F401
183
-
184
- has_recent_cftime = parse_version(cftime.__version__) > parse_version(
185
- "1.1.0"
186
- )
187
- except ImportError:
188
- has_recent_cftime = False
189
-
190
- requires_recent_cftime = pytest.mark.skipif(
191
- not has_recent_cftime, reason="requires cftime > 1.1.0"
192
- )
193
-
194
-
195
164
  @pytest.fixture()
196
165
  def golden():
197
166
  return Location(39.742476, -105.1786, 'America/Denver', 1830.14)