# -*- python -*-
#
# Copyright 2016-2025 Inria - CIRAD - INRAe
#
# Distributed under the Cecill-C License.
# See accompanying file LICENSE.txt or copy at
# http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html
#
# WebSite : https://github.com/openalea/astk
#
# File author(s): Christian Fournier <christian.fournier@inrae.fr>
#
# ==============================================================================
""" Equation for determining global horizontal irradiance (GHI), PPFD,
direct normal irradiance (DNI) and diffuse horizontal irradiance (DHI) under clearsky
condition or estimate them from meteorological data
This module is mainly a collection of syntactic sugar to pvlib clearsky and
irradiances packages.
"""
import numpy
import pandas
import warnings
try:
import pvlib
except ImportError:
warnings.warn('pvlib not installed: using pure python, but less accurate, functions')
if pvlib:
from openalea.astk.sun_position import (
sun_position,
sun_extraradiation
)
else:
from openalea.astk.sun_position_astk import (
sun_position,
sun_extraradiation,
)
# default location and dates
_daydate = '2000-06-21'
_timezone = 'Europe/Paris'
_longitude = 3.52
_latitude = 43.36
_altitude = 56
[docs]
def horizontal_irradiance(d_luminance, elevation):
"""horizontal irradiance of a source emitting a given directional luminance
"""
return d_luminance * numpy.sin(numpy.radians(elevation))
[docs]
def directional_luminance(h_irradiance, elevation):
""" directional luminance of a source producing a given horizontal irradiance
"""
return h_irradiance / numpy.sin(numpy.radians(elevation))
[docs]
def air_mass(zenith, altitude=0, with_pvlib=True):
"""Estimate the pressure-corrected air mass
(optical path length relative to zenital path at a location)
Args:
zenith : an array-like object of zenital directions (degrees)
altitude : (float)
with_pvlib : Should we use pvlib library to estimate air mass ?
"""
if pvlib and with_pvlib:
airmass = pvlib.atmosphere.get_relative_airmass(zenith)
pressure = pvlib.atmosphere.alt2pres(altitude)
am = pvlib.atmosphere.get_absolute_airmass(airmass, pressure)
else:
am = 1.0 / numpy.cos(numpy.radians(zenith))
return am
[docs]
def all_weather_sky_clearness(dni, dhi, sun_zenith):
"""Sky clearness as defined in all_weather sky model (Perez et al. 1993)
Args:
dni: direct normal irradiance
dhi:diffuse horizontal irradiance
sun_zenith: zenith angle of the sun (deg)
Returns:
sky clearness
Details:
R. Perez, R. Seals, J. Michalsky, "All-weather model for sky luminance distribution—Preliminary configuration and
validation", Solar Energy, Volume 50, Issue 3, 1993, Pages 235-245,
"""
z = numpy.radians(sun_zenith)
return ((dhi + dni) / dhi + 1.041 * z**3) / (1 + 1.041 * z**3)
[docs]
def all_weather_sky_brightness(dates, dhi, sun_zenith, altitude=0, with_pvlib=True):
"""Sky brightness as defined in all_weather sky model (Perez et al. 1993)
Args:
dates: A pandas datetime index (as generated by pandas.date_range)
dhi: diffuse horizontal irradiance
sun_zenith: zenith angle of the sun (deg)
altitude: altitude of the location
with_pvlib : Should we use pvlib library to estimate air mass ?
Returns:
sky brightness
Details:
R. Perez, R. Seals, J. Michalsky, "All-weather model for sky luminance distribution—Preliminary configuration and
validation", Solar Energy, Volume 50, Issue 3, 1993, Pages 235-245.
"""
am = air_mass(sun_zenith, altitude, with_pvlib=with_pvlib)
dni_extra = sun_extraradiation(dates)
return am * dhi / dni_extra
[docs]
def f_clear_sky(epsilon):
""" Clear-sky / overcast mixing fractions for blended sky irradiance model
ref : J. Mardaljevic. Daylight Simulation: Validation, Sky Models and
Daylight Coefficients. PhD thesis, De Montfort University,
Leicester, UK, 2000.
p193,eq. 5-10
Args:
epsilon: all weather sky clearness
"""
return numpy.minimum(1, (epsilon - 1) / (1.41 - 1))
[docs]
def clearness_index(dates, ghi):
"""Clearness index (Liu and Jordan 1960)
Args:
dates: A pandas datetime index (as generated by pandas.date_range)
ghi: global horizontal irradiance
Returns:
clearness index
Details:
Benjamin Y.H. Liu, Richard C. Jordan, "The interrelationship and characteristic distribution of direct, diffuse
and total solar radiation", Solar Energy, Volume 4, Issue 3, 1960, Pages 1-19.
"""
dni_extra = sun_extraradiation(dates)
return ghi / dni_extra
[docs]
def spitters_daily_diffuse_fraction(ghi, daydate=_daydate):
""" estimate the diffuse fraction using daily averages of global horizontal irradiance"""
So = sun_extraradiation(daydate=daydate).mean()
RsRso = ghi / So
return numpy.where(RsRso <= 0.07, 1,
numpy.where(RsRso <= 0.35, 1 - 2.3 * (RsRso - 0.07) ** 2,
numpy.where(RsRso <= 0.75, 1.33 - 1.46 * RsRso,
0.23)))
[docs]
def micromol_per_joule(dates, ghi, sun_elevation, temp_dew=None):
"""Conversion factor between micromol of PAR and Joule of broadband shortwave solar radiation (Alados et al. 1996)
Args:
dates: A pandas datetime index (as generated by pandas.date_range)
ghi: global horizontal irradiance (W.m-2)
sun_elevation: the elevation angle of the sun (deg)
temp_dew: the dew point temperature (°C) (optional, yields better estimates)
Details:
I. Alados, I. Foyo-Moreno, L. Alados-Arboledas, "Photosynthetically active radiation: measurements and modelling",
Agricultural and Forest Meteorology, Volume 78, Issues 1–2, 1996, Pages 121-131,
"""
beta = numpy.radians(sun_elevation)
kt = clearness_index(dates, ghi)
if temp_dew is None:
return 1.832 - 0.191 * numpy.log(kt) + 0.099 * numpy.sin(beta)
else:
return 1.791 - 0.190 * numpy.log(kt) + 0.005 * temp_dew + 0.049 * numpy.sin(beta)
[docs]
def clear_sky_irradiances(dates=None, daydate=_daydate, longitude=_longitude,
latitude=_latitude, altitude=_altitude,
timezone=_timezone, with_pvlib=True):
""" Estimate component of sky irradiance for clear sky conditions
Args:
dates: A pandas datetime index (as generated by pandas.date_range). If
None, daydate is used.
daydate: (str) yyyy-mm-dd (not used if dates is not None).
longitude: (float) in degrees
latitude: (float) in degrees
altitude: (float) in meter
timezone:(str) the time zone (not used if dates are already localised)
with_pvlib : Should we use pvlib library to estimate clearsky ?
Returns:
a pandas dataframe with global horizontal irradiance, direct normal
irradiance and diffuse horizontal irradiance.
Details:
if with_pvlib is True, the Ineichen (2002) model is used, otherwise GHI is computed after Haurwitz (1945) and DNI
after Meinel (1976).
P. Ineichen and R. Perez, "A New airmass independent formulation for the Linke turbidity coefficient",
Solar Energy, vol 73, pp. 151-157, 2002
B. Haurwitz, "Insolation in Relation to Cloudiness and Cloud Density", Journal of Meteorology, vol. 2,
pp. 154-166, 1945.
A. B. Meinel and M. P. Meinel, Applied solar energy. Reading, MA: Addison-Wesley Publishing Co., 1976
"""
location = dict(latitude=latitude,
longitude=longitude,
altitude=altitude,
timezone=timezone)
df = sun_position(dates=dates, daydate=daydate, **location)
am = air_mass(df['zenith'], altitude, with_pvlib=with_pvlib)
dni_extra = sun_extraradiation(df.index)
if pvlib and with_pvlib:
tl = pvlib.clearsky.lookup_linke_turbidity(df.index, latitude,
longitude)
clearsky = pvlib.clearsky.ineichen(df['zenith'], am, tl,
dni_extra=dni_extra,
altitude=altitude)
clearsky = pandas.concat([df, clearsky], axis=1)
else:
clearsky = df
z = numpy.radians(df['zenith'])
clearsky['ghi'] = 1098 * numpy.cos(z) * numpy.exp(-0.057 / numpy.cos(z))
clearsky['dni'] = dni_extra * numpy.power(0.7, numpy.power(am, 0.678))
clearsky['dhi'] = clearsky['ghi'] - horizontal_irradiance(clearsky['dni'], df['elevation'])
return clearsky.loc[:, ['ghi', 'dni', 'dhi']]
[docs]
def actual_sky_irradiances(dates=None, daydate=_daydate, ghi=None,
attenuation=None,
pressure=101325, temp_dew=None, longitude=_longitude,
latitude=_latitude, altitude=_altitude,
timezone=_timezone, with_pvlib=True):
""" Estimate component of sky irradiances from measured actual global
horizontal irradiance or attenuated clearsky conditions.
Args:
dates: A pandas datetime index (as generated by pandas.date_range). If
None, daydate is used.
daydate: (str) yyyy-mm-dd (not used if dates is not None).
ghi: (array_like) : global horizontal irradiance (W. m-2).If None
(default) clear_sky irradiance are used
attenuation: (float) a attenuation factor for ghi (actual_ghi =
attenuation * ghi). If None (default), no attenuation is applied
pressure: the site pressure (Pa) (for dirint model)
temp_dew: the dew point temperature (dirint model)
longitude: (float) in degrees
latitude: (float) in degrees
altitude: (float) in meter
timezone:(str) the time zone (not used if dates are already localised)
with_pvlib : Should we use pvlib library to estimate sky irradiances ?
Returns:
a pandas dataframe with global horizontal irradiance, direct normal
irradiance and diffuse horizontal irradiance.
Details:
if with_pvlib is True, the 'dirint' model of Perez (1992) is used, otherwise the model of Spitters (1986) is used
Perez, R., P. Ineichen, E. Maxwell, R. Seals and A. Zelenka, (1992). "Dynamic Global-to-Direct Irradiance
Conversion Models", ASHRAE Transactions-Research Series, pp. 354-369
Spitters CJT, Toussaint HAJM, Goudriaan J (1986) "Separating the diffuse and direct component of global
radiation and its implications for modeling canopy photosynthesis.Part I.Components of incoming radiation",
Agricultural and Forest Meteorology 38: 217-229.
"""
location = dict(latitude=latitude,
longitude=longitude,
altitude=altitude,
timezone=timezone)
df = sun_position(dates=dates, daydate=daydate, **location)
if ghi is None:
cs = clear_sky_irradiances(dates=df.index, **location, with_pvlib=with_pvlib)
ghi = cs['ghi']
df['ghi'] = ghi
if attenuation is not None:
df.ghi *= attenuation
if pvlib and with_pvlib:
df['dni'] = pvlib.irradiance.dirint(df.ghi, 90 - df.elevation, df.index,
pressure=pressure, temp_dew=temp_dew)
df['dhi'] = df.ghi - horizontal_irradiance(df.dni, df.elevation)
else:
Io = sun_extraradiation(df.index)
costheta = numpy.sin(numpy.radians(df.elevation))
So = Io * costheta
RsRso = df.ghi / So
R = 0.847 - 1.61 * costheta + 1.04 * costheta * costheta
K = (1.47 - R) / 1.66
RdRs = numpy.where(RsRso <= 0.22, 1,
numpy.where(RsRso <= 0.35,
1 - 6.4 * (RsRso - 0.22) ** 2,
numpy.where(RsRso <= K,
1.47 - 1.66 * RsRso,
R)))
df['dhi'] = df.ghi * RdRs
df['dni'] = directional_luminance(df['ghi'] - df['dhi'], df['elevation'])
return df.loc[:, ('ghi', 'dhi', 'dni')]
[docs]
def sky_irradiance(dates=None, ghi=None, dhi=None, ppfd=None,
daydate=_daydate, day_ghi=None,
attenuation=None,
pressure=101325, temp_dew=None, longitude=_longitude,
latitude=_latitude, altitude=_altitude,
timezone=_timezone, with_pvlib=True):
""" Estimate variables related to sky irradiance.
Args:
dates: A pandas datetime index (as generated by pandas.date_range). If
None, daydate is used.
ghi: (array_like) : global horizontal irradiance (W. m-2).If None
(default) clear_sky irradiance are used
dhi: (array-like): diffuse horizontal irradiance
ppfd: (array-like) photosynthetically active photon flux density (micromol.m-2.s-1) component of ghi. If None
(default), ppfd will be estimated as a function of meteoroligcal conditions and ghi
daydate: (str) yyyy-mm-dd (only used if dates is None).
day_ghi (float): Daily global horizontal irradiance (MJ). Only used if dates is None. If None
(default), clear sky irradiance are used
attenuation: (float) a attenuation factor for ghi or daily_global (actual_ghi =
attenuation * ghi). If None (default), no attenuation is applied. If
dhi is not None, this parameter is not taken into account.
pressure: the site pressure (Pa) (for dirint model)
temp_dew: the dew point temperature (dirint model)
longitude: (float) in degrees
latitude: (float) in degrees
altitude: (float) in meter
timezone:(str) the time zone (not used if dates are already localised)
with_pvlib : Should we use pvlib library to estimate sky irradiances ?
Returns:
a pandas dataframe with azimuth, zenital and elevation angle of the sun, global horizontal irradiance, direct
normal irradiance and diffuse horizontal irradiance of the sky.
"""
location = dict(latitude=latitude,
longitude=longitude,
altitude=altitude,
timezone=timezone)
df = sun_position(dates=dates, daydate=daydate, **location)
if len(df) < 1: # night
if ghi is not None: # twilight conditions (sun_el < 0, ghi > 0)
df = sun_position(dates=dates, daydate=daydate, filter_night=False, **location)
df['ghi'] = ghi
df['dhi'] = ghi
df['dni'] = 0
df = df.loc[df.ghi > 0, :]
else:
df['ghi'] = 0
df['dhi'] = 0
df['dni'] = 0
else: # day
if dates is None and day_ghi is not None:
cs = clear_sky_irradiances(daydate=daydate, with_pvlib=with_pvlib, **location)
mj_cs = cs.ghi.sum() * 3600 / 1e6
ghi = cs.ghi * day_ghi / mj_cs
if ghi is None or dhi is None:
irr = actual_sky_irradiances(dates=df.index, ghi=ghi,
attenuation=attenuation, pressure=pressure,
temp_dew=temp_dew, with_pvlib=with_pvlib, **location)
df = pandas.concat([df, irr], axis=1)
else:
df['ghi'] = ghi
df['dhi'] = dhi
df['dni'] = directional_luminance(numpy.array(ghi) - numpy.array(dhi), df.elevation)
if ppfd is None:
ppfd = df.ghi * micromol_per_joule(df.index, df.ghi, df.elevation, temp_dew=temp_dew)
df['ppfd'] = ppfd
return df.loc[:,
['azimuth', 'zenith', 'elevation', 'ghi', 'dni', 'dhi', 'ppfd']]