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/irradiance.py CHANGED
@@ -73,7 +73,7 @@ def get_extra_radiation(datetime_or_doy, solar_constant=1366.1,
73
73
  Clear Sky Models: Implementation and Analysis", Sandia National
74
74
  Laboratories, SAND2012-2389, 2012.
75
75
 
76
- .. [2] <http://solardat.uoregon.edu/SolarRadiationBasics.html>, Eqs.
76
+ .. [2] http://solardata.uoregon.edu/SolarRadiationBasics.html, Eqs.
77
77
  SR1 and SR2
78
78
 
79
79
  .. [3] Partridge, G. W. and Platt, C. M. R. 1976. Radiative Processes
@@ -551,13 +551,16 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse):
551
551
 
552
552
 
553
553
  def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
554
- '''
555
- Estimate diffuse irradiance from ground reflections given
556
- irradiance, albedo, and surface tilt.
554
+ r'''
555
+ Estimate diffuse irradiance on a tilted surface from ground reflections.
556
+
557
+ Ground diffuse irradiance is calculated as
558
+
559
+ .. math::
560
+
561
+ G_{ground} = GHI \times \rho \times \frac{1 - \cos\beta}{2}
557
562
 
558
- Function to determine the portion of irradiance on a tilted surface
559
- due to ground reflections. Any of the inputs may be DataFrames or
560
- scalars.
563
+ where :math:`\rho` is ``albedo`` and :math:`\beta` is ``surface_tilt``.
561
564
 
562
565
  Parameters
563
566
  ----------
@@ -567,13 +570,13 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
567
570
  (e.g. surface facing up = 0, surface facing horizon = 90).
568
571
 
569
572
  ghi : numeric
570
- Global horizontal irradiance. [W/m^2]
573
+ Global horizontal irradiance. :math:`W/m^2`
571
574
 
572
575
  albedo : numeric, default 0.25
573
576
  Ground reflectance, typically 0.1-0.4 for surfaces on Earth
574
577
  (land), may increase over snow, ice, etc. May also be known as
575
578
  the reflection coefficient. Must be >=0 and <=1. Will be
576
- overridden if surface_type is supplied.
579
+ overridden if ``surface_type`` is supplied.
577
580
 
578
581
  surface_type : string, optional
579
582
  If supplied, overrides ``albedo``. ``surface_type`` can be one of
@@ -584,23 +587,23 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
584
587
  Returns
585
588
  -------
586
589
  grounddiffuse : numeric
587
- Ground reflected irradiance. [W/m^2]
590
+ Ground reflected irradiance. :math:`W/m^2`
588
591
 
592
+ Notes
593
+ -----
594
+ Table of albedo values by ``surface_type`` are from [2]_, [3]_, [4]_;
595
+ see :py:data:`~pvlib.irradiance.SURFACE_ALBEDOS`.
589
596
 
590
597
  References
591
598
  ----------
592
599
  .. [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute
593
600
  solar irradiance on inclined surfaces for building energy simulation"
594
601
  2007, Solar Energy vol. 81. pp. 254-267.
595
-
596
- The calculation is the last term of equations 3, 4, 7, 8, 10, 11, and 12.
597
-
598
- .. [2] albedos from:
599
- http://files.pvsyst.com/help/albedo.htm
600
- and
601
- http://en.wikipedia.org/wiki/Albedo
602
- and
603
- https://doi.org/10.1175/1520-0469(1972)029<0959:AOTSS>2.0.CO;2
602
+ .. [2] https://www.pvsyst.com/help/albedo.htm Accessed January, 2024.
603
+ .. [3] http://en.wikipedia.org/wiki/Albedo Accessed January, 2024.
604
+ .. [4] Payne, R. E. "Albedo of the Sea Surface". J. Atmos. Sci., 29,
605
+ pp. 959–970, 1972.
606
+ :doi:`10.1175/1520-0469(1972)029<0959:AOTSS>2.0.CO;2`
604
607
  '''
605
608
 
606
609
  if surface_type is not None:
@@ -1555,8 +1558,8 @@ def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
1555
1558
  albedo : numeric, default 0.25
1556
1559
  Ground surface albedo. [unitless]
1557
1560
  xtol : numeric, default 0.01
1558
- Convergence criterion. The estimated GHI will be within xtol of the
1559
- true value. [W/m^2]
1561
+ Convergence criterion. The estimated GHI will be within xtol of the
1562
+ true value. Must be positive. [W/m^2]
1560
1563
  full_output : boolean, default False
1561
1564
  If full_output is False, only ghi is returned, otherwise the return
1562
1565
  value is (ghi, converged, niter). (see Returns section for details).
@@ -1591,13 +1594,16 @@ def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
1591
1594
  '''
1592
1595
  # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023
1593
1596
 
1597
+ if xtol <= 0:
1598
+ raise ValueError(f"xtol too small ({xtol:g} <= 0)")
1599
+
1594
1600
  ghi_from_poa_array = np.vectorize(_ghi_from_poa)
1595
1601
 
1596
1602
  ghi, conv, niter = ghi_from_poa_array(surface_tilt, surface_azimuth,
1597
1603
  solar_zenith, solar_azimuth,
1598
1604
  poa_global,
1599
1605
  dni_extra, airmass, albedo,
1600
- xtol=0.01)
1606
+ xtol=xtol)
1601
1607
 
1602
1608
  if isinstance(poa_global, pd.Series):
1603
1609
  ghi = pd.Series(ghi, poa_global.index)
pvlib/location.py CHANGED
@@ -439,8 +439,10 @@ def lookup_altitude(latitude, longitude):
439
439
  # 255 is a special value that means nodata. Fallback to 0 if nodata.
440
440
  if alt == 255:
441
441
  return 0
442
+ # convert from np.uint8 to float so that the following operations succeed
443
+ alt = float(alt)
442
444
  # Altitude is encoded in 28 meter steps from -450 meters to 6561 meters
443
445
  # There are 0-254 possible altitudes, with 255 reserved for nodata.
444
446
  alt *= 28
445
447
  alt -= 450
446
- return float(alt)
448
+ return alt
pvlib/modelchain.py CHANGED
@@ -1117,10 +1117,7 @@ class ModelChain:
1117
1117
  temperature_model_parameters = tuple(
1118
1118
  array.temperature_model_parameters for array in self.system.arrays)
1119
1119
  params = _common_keys(temperature_model_parameters)
1120
- # remove or statement in v0.9
1121
- if {'a', 'b', 'deltaT'} <= params or (
1122
- not params and self.system.racking_model is None
1123
- and self.system.module_type is None):
1120
+ if {'a', 'b', 'deltaT'} <= params:
1124
1121
  return self.sapm_temp
1125
1122
  elif {'u_c', 'u_v'} <= params:
1126
1123
  return self.pvsyst_temp
@@ -1131,11 +1128,15 @@ class ModelChain:
1131
1128
  elif {'noct', 'module_efficiency'} <= params:
1132
1129
  return self.noct_sam_temp
1133
1130
  else:
1134
- raise ValueError(f'could not infer temperature model from '
1135
- f'system.temperature_model_parameters. Check '
1136
- f'that all Arrays in system.arrays have '
1137
- f'parameters for the same temperature model. '
1138
- f'Common temperature model parameters: {params}.')
1131
+ raise ValueError('Could not infer temperature model from '
1132
+ 'ModelChain.system. '
1133
+ 'If Arrays are used to construct the PVSystem, '
1134
+ 'check that all Arrays in '
1135
+ 'ModelChain.system.arrays '
1136
+ 'have parameters for the same temperature model. '
1137
+ 'If Arrays are not used, check that the PVSystem '
1138
+ 'attributes `racking_model` and `module_type` '
1139
+ 'are valid.')
1139
1140
 
1140
1141
  def _set_celltemp(self, model):
1141
1142
  """Set self.results.cell_temperature using the given cell
pvlib/pvarray.py CHANGED
@@ -223,3 +223,130 @@ def fit_pvefficiency_adr(effective_irradiance, temp_cell, eta,
223
223
  return dict(zip(P_NAMES, popt))
224
224
  else:
225
225
  return popt
226
+
227
+
228
+ def _infer_k_huld(cell_type, pdc0):
229
+ # from PVGIS documentation, "PVGIS data sources & calculation methods",
230
+ # Section 5.2.3, accessed 12/22/2023
231
+ # The parameters in PVGIS' documentation are for a version of Huld's
232
+ # equation that has factored Pdc0 out of the polynomial:
233
+ # P = G/1000 * Pdc0 * (1 + k1 log(Geff) + ...) so these parameters are
234
+ # multiplied by pdc0
235
+ huld_params = {'csi': (-0.017237, -0.040465, -0.004702, 0.000149,
236
+ 0.000170, 0.000005),
237
+ 'cis': (-0.005554, -0.038724, -0.003723, -0.000905,
238
+ -0.001256, 0.000001),
239
+ 'cdte': (-0.046689, -0.072844, -0.002262, 0.000276,
240
+ 0.000159, -0.000006)}
241
+ k = tuple([x*pdc0 for x in huld_params[cell_type.lower()]])
242
+ return k
243
+
244
+
245
+ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None):
246
+ r"""
247
+ Power (DC) using the Huld model.
248
+
249
+ The Huld model [1]_ is used by PVGIS and is given by
250
+
251
+
252
+ .. math::
253
+
254
+ P_{dc} &= G' ( P_{dc0} + k_1 \log(G') + k_2 \log^2 (G') + k_3 T' +
255
+ k_4 T' \log(G') + k_5 T' \log^2 (G') + k_6 T'^2)
256
+
257
+ G' &= \frac{G_{poa eff}}{1000}
258
+
259
+ T' &= T_{mod} - 25^{\circ}C
260
+
261
+
262
+ Parameters
263
+ ----------
264
+ effective_irradiance : numeric
265
+ The irradiance that is converted to photocurrent. [:math:`W/m^2`]
266
+ temp_mod: numeric
267
+ Module back-surface temperature. [C]
268
+ pdc0: numeric
269
+ Power of the modules at reference conditions 1000 :math:`W/m^2`
270
+ and :math:`25^{\circ}C`. [W]
271
+ k : tuple, optional
272
+ Empirical coefficients used in the power model. Length 6. If ``k`` is
273
+ not provided, ``cell_type`` must be specified.
274
+ cell_type : str, optional
275
+ If provided, must be one of ``'cSi'``, ``'CIS'``, or ``'CdTe'``.
276
+ Used to look up default values for ``k`` if ``k`` is not specified.
277
+
278
+ Returns
279
+ -------
280
+ pdc: numeric
281
+ DC power. [W]
282
+
283
+ Raises
284
+ ------
285
+ ValueError
286
+ If neither ``k`` nor ``cell_type`` are specified.
287
+
288
+ Notes
289
+ -----
290
+ The equation for :math:`P_{dc}` is from [1]_. The expression used in PVGIS
291
+ documentation differs by factoring :math:`P_{dc0}` out of the
292
+ polynomial:
293
+
294
+ .. math::
295
+
296
+ P_{dc} = G' P_{dc0} (1 + k'_1 \log(G') + k'_2 \log^2 (G') + k'_3 T' +
297
+ k'_4 T' \log(G') + k'_5 T' \log^2 (G') + k'_6 T'^2)
298
+
299
+
300
+ PVGIS documentation shows a table of default parameters :math:`k'` for
301
+ different cell types. The parameters :math:`k'` differ from the parameters
302
+ :math:`k` for :py:func:`huld` by the factor ``pdc0``, that is,
303
+
304
+ .. math::
305
+
306
+ k = P_{dc0} k'
307
+
308
+ With default values for :math:`k`, at very low irradiance, i.e.,
309
+ :math:`G' < 20 W/m^2`, :math:`P_{dc}` may be negative
310
+ due to the terms involving :math:`\log(G')`.
311
+
312
+ :py:func:`huld` is a component of the PV performance model implemented in
313
+ PVGIS. Among other components, the full PVGIS model includes:
314
+
315
+ - the Faiman model for module temperature
316
+ :py:func:`pvlib.temperature.faiman`
317
+ - the Martin and Ruiz model for the incidence angle modifier (IAM)
318
+ :py:func:`pvlib.iam.martin_ruiz`
319
+ - a custom model for a spectral adjustment factor
320
+
321
+ The PVGIS API (see :py:func:`pvlib.iotools.get_pvgis_hourly`) returns
322
+ broadband plane-of-array irradiance (``poa_global``) and DC power (``P``).
323
+ ``poa_global`` is irradiance before applying the IAM and spectral
324
+ adjustments. In contrast the ``effective_irradiance`` for :py:func:`huld`
325
+ should have the IAM and spectral adjustments. Users comparing output of
326
+ :py:func:`huld` to PVGIS' ``P`` values should expect differences unless
327
+ ``effective_irradiance`` is computed in the same way as done by PVGIS.
328
+
329
+ References
330
+ ----------
331
+ .. [1] T. Huld, G. Friesen, A. Skoczek, R. Kenny, T. Sample, M. Field,
332
+ E. Dunlop. A power-rating model for crystalline silicon PV modules.
333
+ Solar Energy Materials and Solar Cells 95, (2011), pp. 3359-3369.
334
+ :doi:`10.1016/j.solmat.2011.07.026`.
335
+ """
336
+ if k is None:
337
+ if cell_type is not None:
338
+ k = _infer_k_huld(cell_type, pdc0)
339
+ else:
340
+ raise ValueError('Either k or cell_type must be specified')
341
+
342
+ gprime = effective_irradiance / 1000
343
+ tprime = temp_mod - 25
344
+ # accomodate gprime<=0
345
+ with np.errstate(divide='ignore'):
346
+ logGprime = np.log(gprime, out=np.zeros_like(gprime),
347
+ where=np.array(gprime > 0))
348
+ # Eq. 1 in [1]
349
+ pdc = gprime * (pdc0 + k[0] * logGprime + k[1] * logGprime**2 +
350
+ k[2] * tprime + k[3] * tprime * logGprime +
351
+ k[4] * tprime * logGprime**2 + k[5] * tprime**2)
352
+ return pdc
pvlib/pvsystem.py CHANGED
@@ -7,7 +7,7 @@ from collections import OrderedDict
7
7
  import functools
8
8
  import io
9
9
  import itertools
10
- import os
10
+ from pathlib import Path
11
11
  import inspect
12
12
  from urllib.request import urlopen
13
13
  import numpy as np
@@ -1487,8 +1487,10 @@ def calcparams_desoto(effective_irradiance, temp_cell,
1487
1487
  '''
1488
1488
  Calculates five parameter values for the single diode equation at
1489
1489
  effective irradiance and cell temperature using the De Soto et al.
1490
- model described in [1]_. The five values returned by calcparams_desoto
1491
- can be used by singlediode to calculate an IV curve.
1490
+ model. The five values returned by ``calcparams_desoto`` can be used by
1491
+ singlediode to calculate an IV curve.
1492
+
1493
+ The model is described in [1]_.
1492
1494
 
1493
1495
  Parameters
1494
1496
  ----------
@@ -1958,9 +1960,9 @@ def calcparams_pvsyst(effective_irradiance, temp_cell,
1958
1960
 
1959
1961
 
1960
1962
  def retrieve_sam(name=None, path=None):
1961
- '''
1962
- Retrieve latest module and inverter info from a local file or the
1963
- SAM website.
1963
+ """
1964
+ Retrieve latest module and inverter info from a file bundled with pvlib,
1965
+ a path or an URL (like SAM's website).
1964
1966
 
1965
1967
  This function will retrieve either:
1966
1968
 
@@ -1971,10 +1973,14 @@ def retrieve_sam(name=None, path=None):
1971
1973
 
1972
1974
  and return it as a pandas DataFrame.
1973
1975
 
1976
+ .. note::
1977
+ Only provide one of ``name`` or ``path``.
1978
+
1974
1979
  Parameters
1975
1980
  ----------
1976
1981
  name : string, optional
1977
- Name can be one of:
1982
+ Use one of the following strings to retrieve a database bundled with
1983
+ pvlib:
1978
1984
 
1979
1985
  * 'CECMod' - returns the CEC module database
1980
1986
  * 'CECInverter' - returns the CEC Inverter database
@@ -1985,7 +1991,7 @@ def retrieve_sam(name=None, path=None):
1985
1991
  * 'ADRInverter' - returns the ADR Inverter database
1986
1992
 
1987
1993
  path : string, optional
1988
- Path to the SAM file. May also be a URL.
1994
+ Path to a CSV file or a URL.
1989
1995
 
1990
1996
  Returns
1991
1997
  -------
@@ -1997,7 +2003,11 @@ def retrieve_sam(name=None, path=None):
1997
2003
  Raises
1998
2004
  ------
1999
2005
  ValueError
2000
- If no name or path is provided.
2006
+ If no ``name`` or ``path`` is provided.
2007
+ ValueError
2008
+ If both ``name`` and ``path`` are provided.
2009
+ KeyError
2010
+ If the provided ``name`` is not a valid database name.
2001
2011
 
2002
2012
  Notes
2003
2013
  -----
@@ -2030,38 +2040,38 @@ def retrieve_sam(name=None, path=None):
2030
2040
  CEC_Date NaN
2031
2041
  CEC_Type Utility Interactive
2032
2042
  Name: AE_Solar_Energy__AE6_0__277V_, dtype: object
2033
- '''
2034
-
2035
- if name is not None:
2036
- name = name.lower()
2037
- data_path = os.path.join(
2038
- os.path.dirname(os.path.abspath(__file__)), 'data')
2039
- if name == 'cecmod':
2040
- csvdata = os.path.join(
2041
- data_path, 'sam-library-cec-modules-2019-03-05.csv')
2042
- elif name == 'sandiamod':
2043
- csvdata = os.path.join(
2044
- data_path, 'sam-library-sandia-modules-2015-6-30.csv')
2045
- elif name == 'adrinverter':
2046
- csvdata = os.path.join(
2047
- data_path, 'adr-library-cec-inverters-2019-03-05.csv')
2048
- elif name in ['cecinverter', 'sandiainverter']:
2049
- # Allowing either, to provide for old code,
2050
- # while aligning with current expectations
2051
- csvdata = os.path.join(
2052
- data_path, 'sam-library-cec-inverters-2019-03-05.csv')
2053
- else:
2054
- raise ValueError(f'invalid name {name}')
2055
- elif path is not None:
2056
- if path.startswith('http'):
2057
- response = urlopen(path)
2058
- csvdata = io.StringIO(response.read().decode(errors='ignore'))
2059
- else:
2060
- csvdata = path
2043
+ """
2044
+ # error: path was previously silently ignored if name was given GH#2018
2045
+ if name is not None and path is not None:
2046
+ raise ValueError("Please provide either 'name' or 'path', not both.")
2061
2047
  elif name is None and path is None:
2062
- raise ValueError("A name or path must be provided!")
2063
-
2064
- return _parse_raw_sam_df(csvdata)
2048
+ raise ValueError("Please provide either 'name' or 'path'.")
2049
+ elif name is not None:
2050
+ internal_dbs = {
2051
+ "cecmod": "sam-library-cec-modules-2019-03-05.csv",
2052
+ "sandiamod": "sam-library-sandia-modules-2015-6-30.csv",
2053
+ "adrinverter": "adr-library-cec-inverters-2019-03-05.csv",
2054
+ # Both 'cecinverter' and 'sandiainverter', point to same database
2055
+ # to provide for old code, while aligning with current expectations
2056
+ "cecinverter": "sam-library-cec-inverters-2019-03-05.csv",
2057
+ "sandiainverter": "sam-library-cec-inverters-2019-03-05.csv",
2058
+ }
2059
+ try:
2060
+ csvdata_path = Path(__file__).parent.joinpath(
2061
+ "data", internal_dbs[name.lower()]
2062
+ )
2063
+ except KeyError:
2064
+ raise KeyError(
2065
+ f"Invalid name {name}. "
2066
+ + f"Provide one of {list(internal_dbs.keys())}."
2067
+ ) from None
2068
+ else: # path is not None
2069
+ if path.lower().startswith("http"): # URL check is not case-sensitive
2070
+ response = urlopen(path) # URL is case-sensitive
2071
+ csvdata_path = io.StringIO(response.read().decode(errors="ignore"))
2072
+ else:
2073
+ csvdata_path = path
2074
+ return _parse_raw_sam_df(csvdata_path)
2065
2075
 
2066
2076
 
2067
2077
  def _normalize_sam_product_names(names):
@@ -2254,10 +2264,9 @@ def sapm(effective_irradiance, temp_cell, module):
2254
2264
  module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
2255
2265
  (1 + module['Aisc']*(temp_cell - temp_ref)))
2256
2266
 
2257
- # the Ixx calculation in King 2004 has a typo (mixes up Aisc and Aimp)
2258
2267
  out['i_xx'] = (
2259
2268
  module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
2260
- (1 + module['Aisc']*(temp_cell - temp_ref)))
2269
+ (1 + module['Aimp']*(temp_cell - temp_ref)))
2261
2270
 
2262
2271
  if isinstance(out['i_sc'], pd.Series):
2263
2272
  out = pd.DataFrame(out)
@@ -2536,7 +2545,7 @@ def singlediode(photocurrent, saturation_current, resistance_series,
2536
2545
 
2537
2546
 
2538
2547
  def max_power_point(photocurrent, saturation_current, resistance_series,
2539
- resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf,
2548
+ resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.inf,
2540
2549
  method='brentq'):
2541
2550
  """
2542
2551
  Given the single diode equation coefficients, calculates the maximum power
pvlib/scaling.py CHANGED
@@ -13,8 +13,10 @@ from scipy.spatial.distance import pdist
13
13
  def wvm(clearsky_index, positions, cloud_speed, dt=None):
14
14
  """
15
15
  Compute spatial aggregation time series smoothing on clear sky index based
16
- on the Wavelet Variability model of Lave et al. [1]_, [2]_. Implementation
17
- is basically a port of the Matlab version of the code [3]_.
16
+ on the Wavelet Variability model.
17
+
18
+ This model is described in Lave et al. [1]_, [2]_.
19
+ Implementation is basically a port of the Matlab version of the code [3]_.
18
20
 
19
21
  Parameters
20
22
  ----------
pvlib/shading.py CHANGED
@@ -232,3 +232,113 @@ def sky_diffuse_passias(masking_angle):
232
232
  Available at https://www.nrel.gov/docs/fy18osti/67399.pdf
233
233
  """
234
234
  return 1 - cosd(masking_angle/2)**2
235
+
236
+
237
+ def projected_solar_zenith_angle(solar_zenith, solar_azimuth,
238
+ axis_tilt, axis_azimuth):
239
+ r"""
240
+ Calculate projected solar zenith angle in degrees.
241
+
242
+ This solar zenith angle is projected onto the plane whose normal vector is
243
+ defined by ``axis_tilt`` and ``axis_azimuth``. The normal vector is in the
244
+ direction of ``axis_azimuth`` (clockwise from north) and tilted from
245
+ horizontal by ``axis_tilt``. See Figure 5 in [1]_:
246
+
247
+ .. figure:: ../../_images/Anderson_Mikofski_2020_Fig5.jpg
248
+ :alt: Wire diagram of coordinates systems to obtain the projected angle.
249
+ :align: center
250
+ :scale: 50 %
251
+
252
+ Fig. 5, [1]_: Solar coordinates projection onto tracker rotation plane.
253
+
254
+ Parameters
255
+ ----------
256
+ solar_zenith : numeric
257
+ Sun's apparent zenith in degrees.
258
+ solar_azimuth : numeric
259
+ Sun's azimuth in degrees.
260
+ axis_tilt : numeric
261
+ Axis tilt angle in degrees. From horizontal plane to array plane.
262
+ axis_azimuth : numeric
263
+ Axis azimuth angle in degrees.
264
+ North = 0°; East = 90°; South = 180°; West = 270°
265
+
266
+ Returns
267
+ -------
268
+ Projected_solar_zenith : numeric
269
+ In degrees.
270
+
271
+ Notes
272
+ -----
273
+ This projection has a variety of applications in PV. For example:
274
+
275
+ - Projecting the sun's position onto the plane perpendicular to
276
+ the axis of a single-axis tracker (i.e. the plane
277
+ whose normal vector coincides with the tracker torque tube)
278
+ yields the tracker rotation angle that maximizes direct irradiance
279
+ capture. This tracking strategy is called *true-tracking*. Learn more
280
+ about tracking in
281
+ :ref:`sphx_glr_gallery_solar-tracking_plot_single_axis_tracking.py`.
282
+
283
+ - Self-shading in large PV arrays is often modeled by assuming
284
+ a simplified 2-D array geometry where the sun's position is
285
+ projected onto the plane perpendicular to the PV rows.
286
+ The projected zenith angle is then used for calculations
287
+ regarding row-to-row shading.
288
+
289
+ Examples
290
+ --------
291
+ Calculate the ideal true-tracking angle for a horizontal north-south
292
+ single-axis tracker:
293
+
294
+ >>> rotation = projected_solar_zenith_angle(solar_zenith, solar_azimuth,
295
+ >>> axis_tilt=0, axis_azimuth=180)
296
+
297
+ Calculate the projected zenith angle in a south-facing fixed tilt array
298
+ (note: the ``axis_azimuth`` of a fixed-tilt row points along the length
299
+ of the row):
300
+
301
+ >>> psza = projected_solar_zenith_angle(solar_zenith, solar_azimuth,
302
+ >>> axis_tilt=0, axis_azimuth=90)
303
+
304
+ References
305
+ ----------
306
+ .. [1] K. Anderson and M. Mikofski, 'Slope-Aware Backtracking for
307
+ Single-Axis Trackers', National Renewable Energy Lab. (NREL), Golden,
308
+ CO (United States);
309
+ NREL/TP-5K00-76626, Jul. 2020. :doi:`10.2172/1660126`.
310
+
311
+ See Also
312
+ --------
313
+ pvlib.solarposition.get_solarposition
314
+ """
315
+ # Assume the tracker reference frame is right-handed. Positive y-axis is
316
+ # oriented along tracking axis; from north, the y-axis is rotated clockwise
317
+ # by the axis azimuth and tilted from horizontal by the axis tilt. The
318
+ # positive x-axis is 90 deg clockwise from the y-axis and parallel to
319
+ # horizontal (e.g., if the y-axis is south, the x-axis is west); the
320
+ # positive z-axis is normal to the x and y axes, pointed upward.
321
+
322
+ # Since elevation = 90 - zenith, sin(90-x) = cos(x) & cos(90-x) = sin(x):
323
+ # Notation from [1], modified to use zenith instead of elevation
324
+ # cos(elevation) = sin(zenith) and sin(elevation) = cos(zenith)
325
+ # Avoid recalculating these values
326
+ sind_solar_zenith = sind(solar_zenith)
327
+ cosd_axis_azimuth = cosd(axis_azimuth)
328
+ sind_axis_azimuth = sind(axis_azimuth)
329
+ sind_axis_tilt = sind(axis_tilt)
330
+
331
+ # Sun's x, y, z coords
332
+ sx = sind_solar_zenith * sind(solar_azimuth)
333
+ sy = sind_solar_zenith * cosd(solar_azimuth)
334
+ sz = cosd(solar_zenith)
335
+ # Eq. (4); sx', sz' values from sun coordinates projected onto surface
336
+ sx_prime = sx * cosd_axis_azimuth - sy * sind_axis_azimuth
337
+ sz_prime = (
338
+ sx * sind_axis_azimuth * sind_axis_tilt
339
+ + sy * sind_axis_tilt * cosd_axis_azimuth
340
+ + sz * cosd(axis_tilt)
341
+ )
342
+ # Eq. (5); angle between sun's beam and surface
343
+ theta_T = np.degrees(np.arctan2(sx_prime, sz_prime))
344
+ return theta_T