Skip to content

Commit af8dde5

Browse files
adriessecwhanseAdamRJensen
authored
Continuous version of the Perez transposition model implementation (#1876)
* Definitely not ready for review! * Big step forward. * Add entry in docs. * A working model but just one test sofar. * Add new model as option in get_sky_diffuse. Docstring edits pending. * Completed doc strings. Also a bit of fine-tuning code. * Updated whatsnew. * Bugfix, formatting fix, and add all tests. * Test warning plus some other small changes. * Make flake happy. * Update pvlib/irradiance.py Co-authored-by: Cliff Hansen <[email protected]> * Address comments. * Add contributor code comments. * Update pvlib/irradiance.py Co-authored-by: Adam R. Jensen <[email protected]> * Adapt to reviewer preferences. * Adapt to flake preferences. * Remove model pseudo-option. * Flake --------- Co-authored-by: Cliff Hansen <[email protected]> Co-authored-by: Adam R. Jensen <[email protected]>
1 parent 46851d9 commit af8dde5

File tree

4 files changed

+333
-18
lines changed

4 files changed

+333
-18
lines changed

docs/sphinx/source/reference/irradiance/transposition.rst

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Transposition models
1010
irradiance.get_sky_diffuse
1111
irradiance.isotropic
1212
irradiance.perez
13+
irradiance.perez_driesse
1314
irradiance.haydavies
1415
irradiance.klucher
1516
irradiance.reindl

docs/sphinx/source/whatsnew/v0.10.3.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ v0.10.3 (Anticipated December, 2023)
77

88
Enhancements
99
~~~~~~~~~~~~
10+
* Added the continuous Perez-Driesse transposition model.
11+
:py:func:`pvlib.irradiance.perez_driesse` (:issue:`1841`, :pull:`1876`)
1012
* :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and
1113
:py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` now include
1214
shaded fraction in returned variables. (:pull:`1871`)
@@ -28,4 +30,5 @@ Contributors
2830
~~~~~~~~~~~~
2931
* Arjan Keeman (:ghuser:`akeeman`)
3032
* Miguel Sánchez de León Peque (:ghuser:`Peque`)
31-
* Will Hobbs (:ghuser:`williamhobbs`)
33+
* Will Hobbs (:ghuser:`williamhobbs`)
34+
* Anton Driesse (:ghuser:`adriesse`)

pvlib/irradiance.py

+247-13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import numpy as np
1212
import pandas as pd
13+
from scipy.interpolate import splev
1314

1415
from pvlib import atmosphere, solarposition, tools
1516
import 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+
12151447
def 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

Comments
 (0)