1010
1111import numpy as np
1212import pandas as pd
13+ from scipy .interpolate import splev
1314
1415from pvlib import atmosphere , solarposition , tools
1516import pvlib # used to avoid dni name collision in complete_irradiance
@@ -323,6 +324,7 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
323324 * reindl
324325 * king
325326 * perez
327+ * perez-driesse
326328
327329 Parameters
328330 ----------
@@ -351,7 +353,8 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
351353 the list of accepted values.
352354 model : str, default 'isotropic'
353355 Irradiance model. Can be one of ``'isotropic'``, ``'klucher'``,
354- ``'haydavies'``, ``'reindl'``, ``'king'``, ``'perez'``.
356+ ``'haydavies'``, ``'reindl'``, ``'king'``, ``'perez'``,
357+ ``'perez-driesse'``.
355358 model_perez : str, default 'allsitescomposite1990'
356359 Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.
357360
@@ -363,13 +366,13 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
363366
364367 Notes
365368 -----
366- Models ``'haydavies'``, ``'reindl'``, or ``'perez'`` require
367- ``'dni_extra'``. Values can be calculated using
369+ Models ``'haydavies'``, ``'reindl'``, ``'perez'`` and ``'perez-driesse'``
370+ require ``'dni_extra'``. Values can be calculated using
368371 :py:func:`~pvlib.irradiance.get_extra_radiation`.
369372
370- The ``'perez'`` model requires relative airmass (``airmass``) as input. If
371- ``airmass`` is not provided, it is calculated using the defaults in
372- :py:func:`~pvlib.atmosphere.get_relative_airmass`.
373+ The ``'perez'`` and ``'perez-driesse'`` models require relative airmass
374+ ( ``airmass``) as input. If ``airmass`` is not provided, it is calculated
375+ using the defaults in :py:func:`~pvlib.atmosphere.get_relative_airmass`.
373376 """
374377
375378 poa_sky_diffuse = get_sky_diffuse (
@@ -400,6 +403,7 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
400403 * reindl
401404 * king
402405 * perez
406+ * perez-driesse
403407
404408 Parameters
405409 ----------
@@ -423,7 +427,8 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
423427 Relative airmass (not adjusted for pressure). [unitless]
424428 model : str, default 'isotropic'
425429 Irradiance model. Can be one of ``'isotropic'``, ``'klucher'``,
426- ``'haydavies'``, ``'reindl'``, ``'king'``, ``'perez'``.
430+ ``'haydavies'``, ``'reindl'``, ``'king'``, ``'perez'``,
431+ ``'perez-driesse'``.
427432 model_perez : str, default 'allsitescomposite1990'
428433 Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.
429434
@@ -440,18 +445,19 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
440445
441446 Notes
442447 -----
443- Models ``'haydavies'``, ``'reindl'``, and ``'perez``` require 'dni_extra'.
444- Values can be calculated using
448+ Models ``'haydavies'``, ``'reindl'``, ``'perez'`` and ``'perez-driesse'``
449+ require ``'dni_extra'``. Values can be calculated using
445450 :py:func:`~pvlib.irradiance.get_extra_radiation`.
446451
447- The ``'perez'`` model requires relative airmass (``airmass``) as input. If
448- ``airmass`` is not provided, it is calculated using the defaults in
449- :py:func:`~pvlib.atmosphere.get_relative_airmass`.
452+ The ``'perez'`` and ``'perez-driesse'`` models require relative airmass
453+ ( ``airmass``) as input. If ``airmass`` is not provided, it is calculated
454+ using the defaults in :py:func:`~pvlib.atmosphere.get_relative_airmass`.
450455 """
451456
452457 model = model .lower ()
453458
454- if (model in {'haydavies' , 'reindl' , 'perez' }) and (dni_extra is None ):
459+ if dni_extra is None and model in {'haydavies' , 'reindl' ,
460+ 'perez' , 'perez-driesse' }:
455461 raise ValueError (f'dni_extra is required for model { model } ' )
456462
457463 if model == 'isotropic' :
@@ -473,6 +479,10 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
473479 sky = perez (surface_tilt , surface_azimuth , dhi , dni , dni_extra ,
474480 solar_zenith , solar_azimuth , airmass ,
475481 model = model_perez )
482+ elif model == 'perez-driesse' :
483+ # perez_driesse will calculate its own airmass if needed
484+ sky = perez_driesse (surface_tilt , surface_azimuth , dhi , dni , dni_extra ,
485+ solar_zenith , solar_azimuth , airmass )
476486 else :
477487 raise ValueError (f'invalid model selection { model } ' )
478488
@@ -1212,6 +1222,228 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
12121222 return sky_diffuse
12131223
12141224
1225+ def _calc_delta (dhi , dni_extra , solar_zenith , airmass = None ):
1226+ '''
1227+ Compute the delta parameter, which represents sky dome "brightness"
1228+ in the Perez and Perez-Driesse models.
1229+
1230+ Helper function for perez_driesse transposition.
1231+ '''
1232+ if airmass is None :
1233+ # use the same airmass model as in the original perez work
1234+ airmass = atmosphere .get_relative_airmass (solar_zenith ,
1235+ 'kastenyoung1989' )
1236+
1237+ max_airmass = atmosphere .get_relative_airmass (90 , 'kastenyoung1989' )
1238+ airmass = np .where (solar_zenith >= 90 , max_airmass , airmass )
1239+
1240+ return dhi / (dni_extra / airmass )
1241+
1242+
1243+ def _calc_zeta (dhi , dni , zenith ):
1244+ '''
1245+ Compute the zeta parameter, which represents sky dome "clearness"
1246+ in the Perez-Driesse model.
1247+
1248+ Helper function for perez_driesse transposition.
1249+ '''
1250+ dhi = np .asarray (dhi )
1251+ dni = np .asarray (dni )
1252+
1253+ # first calculate what zeta would be without the kappa correction
1254+ # using eq. 5 and eq. 13
1255+ with np .errstate (invalid = 'ignore' ):
1256+ zeta = dni / (dhi + dni )
1257+
1258+ zeta = np .where (dhi == 0 , 0.0 , zeta )
1259+
1260+ # then apply the kappa correction in a manner analogous to eq. 7
1261+ kappa = 1.041
1262+ kterm = kappa * np .radians (zenith ) ** 3
1263+ zeta = zeta / (1 - kterm * (zeta - 1 ))
1264+
1265+ return zeta
1266+
1267+
1268+ def _f (i , j , zeta ):
1269+ '''
1270+ Evaluate the quadratic splines corresponding to the
1271+ allsitescomposite1990 Perez model look-up table.
1272+
1273+ Helper function for perez_driesse transposition.
1274+ '''
1275+ knots = np .array (
1276+ [0.000 , 0.000 , 0.000 ,
1277+ 0.061 , 0.187 , 0.333 , 0.487 , 0.643 , 0.778 , 0.839 ,
1278+ 1.000 , 1.000 , 1.000 ])
1279+
1280+ coefs = np .array (
1281+ [[- 0.053 , + 0.529 , - 0.028 , - 0.071 , + 0.061 , - 0.019 ],
1282+ [- 0.008 , + 0.588 , - 0.062 , - 0.060 , + 0.072 , - 0.022 ],
1283+ [+ 0.131 , + 0.770 , - 0.167 , - 0.026 , + 0.106 , - 0.032 ],
1284+ [+ 0.328 , + 0.471 , - 0.216 , + 0.069 , - 0.105 , - 0.028 ],
1285+ [+ 0.557 , + 0.241 , - 0.300 , + 0.086 , - 0.085 , - 0.012 ],
1286+ [+ 0.861 , - 0.323 , - 0.355 , + 0.240 , - 0.467 , - 0.008 ],
1287+ [ 1.212 , - 1.239 , - 0.444 , + 0.305 , - 0.797 , + 0.047 ],
1288+ [ 1.099 , - 1.847 , - 0.365 , + 0.275 , - 1.132 , + 0.124 ],
1289+ [+ 0.544 , + 0.157 , - 0.213 , + 0.118 , - 1.455 , + 0.292 ],
1290+ [+ 0.544 , + 0.157 , - 0.213 , + 0.118 , - 1.455 , + 0.292 ],
1291+ [+ 0.000 , + 0.000 , + 0.000 , + 0.000 , + 0.000 , + 0.000 ],
1292+ [+ 0.000 , + 0.000 , + 0.000 , + 0.000 , + 0.000 , + 0.000 ],
1293+ [+ 0.000 , + 0.000 , + 0.000 , + 0.000 , + 0.000 , + 0.000 ]])
1294+
1295+ coefs = coefs .T .reshape ((2 , 3 , 13 ))
1296+
1297+ tck = (knots , coefs [i - 1 , j - 1 ], 2 )
1298+
1299+ return splev (zeta , tck )
1300+
1301+
1302+ def perez_driesse (surface_tilt , surface_azimuth , dhi , dni , dni_extra ,
1303+ solar_zenith , solar_azimuth , airmass = None ,
1304+ return_components = False ):
1305+ '''
1306+ Determine diffuse irradiance from the sky on a tilted surface using
1307+ the continuous Perez-Driesse model.
1308+
1309+ The Perez-Driesse model [1]_ is a reformulation of the 1990 Perez
1310+ model [2]_ that provides continuity of the function and of its first
1311+ derivatives. This is achieved by replacing the look-up table of
1312+ coefficients with quadratic splines.
1313+
1314+ Parameters
1315+ ----------
1316+ surface_tilt : numeric
1317+ Surface tilt angles in decimal degrees. surface_tilt must be >=0
1318+ and <=180. The tilt angle is defined as degrees from horizontal
1319+ (e.g. surface facing up = 0, surface facing horizon = 90)
1320+
1321+ surface_azimuth : numeric
1322+ Surface azimuth angles in decimal degrees. surface_azimuth must
1323+ be >=0 and <=360. The azimuth convention is defined as degrees
1324+ east of north (e.g. North = 0, South=180 East = 90, West = 270).
1325+
1326+ dhi : numeric
1327+ Diffuse horizontal irradiance in W/m^2. dhi must be >=0.
1328+
1329+ dni : numeric
1330+ Direct normal irradiance in W/m^2. dni must be >=0.
1331+
1332+ dni_extra : numeric
1333+ Extraterrestrial normal irradiance in W/m^2.
1334+
1335+ solar_zenith : numeric
1336+ apparent (refraction-corrected) zenith angles in decimal
1337+ degrees. solar_zenith must be >=0 and <=180.
1338+
1339+ solar_azimuth : numeric
1340+ Sun azimuth angles in decimal degrees. solar_azimuth must be >=0
1341+ and <=360. The azimuth convention is defined as degrees east of
1342+ north (e.g. North = 0, East = 90, West = 270).
1343+
1344+ airmass : numeric (optional, default None)
1345+ Relative (not pressure-corrected) airmass values. If airmass is a
1346+ DataFrame it must be of the same size as all other DataFrame
1347+ inputs. The kastenyoung1989 airmass calculation is used internally
1348+ and is also recommended when pre-calculating airmass because
1349+ it was used in the original model development.
1350+
1351+ return_components: bool (optional, default=False)
1352+ Flag used to decide whether to return the calculated diffuse components
1353+ or not.
1354+
1355+ Returns
1356+ --------
1357+ numeric, OrderedDict, or DataFrame
1358+ Return type controlled by `return_components` argument.
1359+ If ``return_components=False``, `sky_diffuse` is returned.
1360+ If ``return_components=True``, `diffuse_components` is returned.
1361+
1362+ sky_diffuse : numeric
1363+ The sky diffuse component of the solar radiation on a tilted
1364+ surface.
1365+
1366+ diffuse_components : OrderedDict (array input) or DataFrame (Series input)
1367+ Keys/columns are:
1368+ * sky_diffuse: Total sky diffuse
1369+ * isotropic
1370+ * circumsolar
1371+ * horizon
1372+
1373+ Notes
1374+ -----
1375+ The Perez-Driesse model can be considered a plug-in replacement for the
1376+ 1990 Perez model using the ``'allsitescomposite1990'`` coefficient set.
1377+ Deviations between the two are very small, as demonstrated in [1]_.
1378+ Other coefficient sets are not supported because the 1990 set is
1379+ based on the largest and most diverse set of empirical data.
1380+
1381+ References
1382+ ----------
1383+ .. [1] A. Driesse, A. Jensen, R. Perez, A Continuous Form of the Perez
1384+ Diffuse Sky Model for Forward and Reverse Transposition, accepted
1385+ for publication in the Solar Energy Journal.
1386+
1387+ .. [2] Perez, R., Ineichen, P., Seals, R., Michalsky, J., Stewart, R.,
1388+ 1990. Modeling daylight availability and irradiance components from
1389+ direct and global irradiance. Solar Energy 44 (5), 271-289.
1390+
1391+ See also
1392+ --------
1393+ perez
1394+ isotropic
1395+ haydavies
1396+ klucher
1397+ reindl
1398+ king
1399+ '''
1400+ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct., 2023
1401+
1402+ delta = _calc_delta (dhi , dni_extra , solar_zenith , airmass )
1403+ zeta = _calc_zeta (dhi , dni , solar_zenith )
1404+
1405+ z = np .radians (solar_zenith )
1406+
1407+ F1 = _f (1 , 1 , zeta ) + _f (1 , 2 , zeta ) * delta + _f (1 , 3 , zeta ) * z
1408+ F2 = _f (2 , 1 , zeta ) + _f (2 , 2 , zeta ) * delta + _f (2 , 3 , zeta ) * z
1409+
1410+ # note the newly recommended upper limit on F1
1411+ F1 = np .clip (F1 , 0 , 0.9 )
1412+
1413+ # lines after this point are identical to the original perez function
1414+ # with some checks removed
1415+
1416+ A = aoi_projection (surface_tilt , surface_azimuth ,
1417+ solar_zenith , solar_azimuth )
1418+ A = np .maximum (A , 0 )
1419+
1420+ B = tools .cosd (solar_zenith )
1421+ B = np .maximum (B , tools .cosd (85 ))
1422+
1423+ # Calculate Diffuse POA from sky dome
1424+ term1 = 0.5 * (1 - F1 ) * (1 + tools .cosd (surface_tilt ))
1425+ term2 = F1 * A / B
1426+ term3 = F2 * tools .sind (surface_tilt )
1427+
1428+ sky_diffuse = np .maximum (dhi * (term1 + term2 + term3 ), 0 )
1429+
1430+ if return_components :
1431+ diffuse_components = OrderedDict ()
1432+ diffuse_components ['sky_diffuse' ] = sky_diffuse
1433+
1434+ # Calculate the different components
1435+ diffuse_components ['isotropic' ] = dhi * term1
1436+ diffuse_components ['circumsolar' ] = dhi * term2
1437+ diffuse_components ['horizon' ] = dhi * term3
1438+
1439+ if isinstance (sky_diffuse , pd .Series ):
1440+ diffuse_components = pd .DataFrame (diffuse_components )
1441+
1442+ return diffuse_components
1443+ else :
1444+ return sky_diffuse
1445+
1446+
12151447def clearsky_index (ghi , clearsky_ghi , max_clearsky_index = 2.0 ):
12161448 """
12171449 Calculate the clearsky index.
@@ -2350,6 +2582,8 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None,
23502582 orgill_hollands
23512583 boland
23522584 """
2585+ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug., 2023
2586+
23532587 # central polynomial coefficients with float64 precision
23542588 p = [+ 12.26911439571261000 ,
23552589 - 16.47050842469730700 ,
0 commit comments