PVGIS features an algorithmic repository, independent from the core API, the CLI and the Web API components.
algorithms ¶
Modules:
| Name | Description |
|---|---|
finkelstein_schafer | |
hargreaves | |
hofierka | |
huld | |
iqbal | |
jenco | |
martin_ruiz | |
milne1921 | |
muneer | |
noaa | |
pelland | |
pvlib | |
skyfield | |
usno | |
finkelstein_schafer ¶
Modules:
| Name | Description |
|---|---|
cumulative_distribution | |
statistics | |
univariate_statistics | |
cumulative_distribution ¶
Functions:
| Name | Description |
|---|---|
calculate_ecdf | Calculate the empirical cumulative distribution function (CDF) of the data, ensuring unique coordinates. |
calculate_long_term_monthly_ecdfs | Calculate the long-term CDF for each month. |
calculate_yearly_monthly_ecdfs | Calculate monthly CDFs for each variable for each month and year. |
calculate_ecdf ¶
Calculate the empirical cumulative distribution function (CDF) of the data, ensuring unique coordinates.
Source code in pvgisprototype/algorithms/finkelstein_schafer/cumulative_distribution.py
@log_function_call
def calculate_ecdf(sample):
"""Calculate the empirical cumulative distribution function (CDF) of the data, ensuring unique coordinates."""
from scipy.stats import ecdf
ecdfs = ecdf(sample)
return DataArray(
ecdfs.cdf.probabilities, coords=[ecdfs.cdf.quantiles], dims=["quantile"]
)
calculate_long_term_monthly_ecdfs ¶
Calculate the long-term CDF for each month.
calculate_yearly_monthly_ecdfs ¶
Calculate monthly CDFs for each variable for each month and year.
Source code in pvgisprototype/algorithms/finkelstein_schafer/cumulative_distribution.py
statistics ¶
Functions:
| Name | Description |
|---|---|
align_and_broadcast | Expand dimensions and broadcast a data array to match the structure of a |
calculate_finkelstein_schafer_statistics | Calculate Finkelstein-Schafer statistics for typical meteorological year |
align_and_broadcast ¶
Expand dimensions and broadcast a data array to match the structure of a template array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_array | The data array that needs to be aligned and broadcasted. | required | |
template_array | The array providing the structure to which data_array should be aligned. | required |
Returns:
| Type | Description |
|---|---|
xarray.DataArray: The broadcasted data array with the same structure as the | |
template_array. | |
Source code in pvgisprototype/algorithms/finkelstein_schafer/statistics.py
@log_function_call
def align_and_broadcast(data_array, reference_array):
"""Expand dimensions and broadcast a data array to match the structure of a
template array.
Parameters
----------
data_array: xarray.DataArray
The data array that needs to be aligned and broadcasted.
template_array: xarray.DataArray
The array providing the structure to which data_array should be aligned.
Returns
-------
xarray.DataArray: The broadcasted data array with the same structure as the
template_array.
"""
return data_array.expand_dims(year=reference_array.year).broadcast_like(
reference_array
)
calculate_finkelstein_schafer_statistics ¶
calculate_finkelstein_schafer_statistics(
location_series_data_array: DataArray | Dataset,
) -> tuple[DataArray, Dataset, DataArray, DataArray]
Calculate Finkelstein-Schafer statistics for typical meteorological year selection.
The Finkelstein-Schafer (FS) statistic quantifies how well a candidate month represents the long-term climate by comparing its cumulative distribution function (CDF) to the multi-year average CDF. Lower FS values indicate months that are more representative of typical conditions.
This implementation follows the ISO 15927-4:2005 standard for determining typical meteorological data and the methodology described in Hall et al. (1978) [1]_.
Algorithm
The Finkelstein-Schafer statistic is computed through the following steps:
-
Daily aggregation: Calculate daily mean values from sub-daily (e.g., hourly) time series data.
-
Long-term CDF: For each calendar month m, compute the empirical cumulative distribution function :math:
\phi(q, m)using daily values aggregated across all years in the dataset. -
Yearly CDFs: For each calendar month m and year y for a quantity q, compute the empirical cumulative distribution function :math:
F(q, m, y)using only the daily values from that specific month and year. -
Finkelstein-Schafer statistic: For each month m and year y for a quantity q, calculate the Finkelstein-Schafer statistic by integrating the absolute difference between the yearly and long-term CDFs:
.. math::
FS(q, m, y) = \sum_{i} |F(q_i, m, y) - \phi(q_i, m)|
where the summation is over all quantile values :math:q_i in the distribution.
- Ranking: For each calendar month m for the quantity q, candidate months from different years are ranked by increasing Finkelstein-Schafer statistic FS(q, m, y). Lower values indicate months that better represent typical conditions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
location_series_data_array | DataArray or Dataset | Time series meteorological data with a temporal dimension (typically hourly or sub-daily resolution). The data should span multiple years to enable meaningful long-term statistics. Acceptable formats include:
The time coordinate should be datetime-like and parseable by pandas. | required |
Returns:
| Name | Type | Description |
|---|---|---|
finkelstein_schafer_statistic | DataArray | The computed Finkelstein-Schafer statistic with dimensions
Lower values indicate months that better represent long-term typical conditions. |
daily_statistics | Dataset | Daily aggregated statistics including mean, min, max, and std computed from the input time series. Contains dimensions |
yearly_monthly_ecdfs | DataArray | Empirical cumulative distribution functions for each individual month-year combination. Dimensions: |
long_term_monthly_ecdfs | DataArray | Long-term empirical cumulative distribution functions computed across all years for each calendar month. Dimensions: |
Notes
The Finkelstein-Schafer statistic is a non-parametric measure that:
- Does not assume any particular distribution shape (e.g., normal, uniform) Accounts for the entire distribution, not just central tendency (mean/median)
- Is robust to outliers and extreme values
- Enables objective selection of typical months based on distributional similarity
Typical use case: This function is the core computational step in generating Typical Meteorological Year (TMY) datasets. After computing FS statistics for multiple meteorological variables (temperature, irradiance, humidity, wind speed), a weighted sum across variables is used to select the most representative month for each calendar month position in the TMY.
Data requirements:
- Minimum 5-10 years of data recommended for stable statistics
- Complete or near-complete temporal coverage (missing data impacts CDF accuracy)
- Sub-daily resolution (hourly preferred) to capture diurnal patterns
References
.. [1] Hall, I. J., Prairie, R. R., Anderson, H. E., & Boes, E. C. (1978). "Generation of a Typical Meteorological Year." Proceedings of the 1978 Annual Meeting of the American Section of the International Solar Energy Society (AS/ISES), Denver, CO.
.. [2] ISO 15927-4:2005. "Hygrothermal performance of buildings - Calculation and presentation of climatic data - Part 4: Hourly data for assessing the annual energy use for heating and cooling."
.. [3] Wilcox, S., & Marion, W. (2008). "Users Manual for TMY3 Data Sets." Technical Report NREL/TP-581-43156, National Renewable Energy Laboratory.
Examples:
>>> import xarray as xr
>>> import numpy as np
>>> import pandas as pd
>>> from pvgisprototype.algorithms.finkelstein_schafer.statistics import (
... calculate_finkelstein_schafer_statistics
... )
>>>
>>> # Create sample hourly temperature data for 3 years
>>> time = pd.date_range('2015-01-01', '2017-12-31 23:00', freq='H')
>>> temperature = 15 + 10 * np.sin(2 * np.pi * np.arange(len(time)) / (365.25 * 24))
>>> temperature += np.random.normal(0, 2, len(time)) # Add noise
>>>
>>> data = xr.DataArray(
... temperature,
... coords={'time': time},
... dims=['time'],
... name='temperature'
... )
>>>
>>> # Calculate Finkelstein-Schafer statistics
>>> fs_statistics, daily_statistics, yearly_ecdfs, long_term_ecdfs = (
... calculate_finkelstein_schafer_statistics(data)
... )
>>>
>>> # Find the most representative January across all years
>>> january_fs = fs_stat.sel(month=1)
>>> best_january_year = january_fs.idxmin(dim='year').values
>>> print(f"Most typical January: {best_january_year}")
>>> print(f"FS statistic: {january_fs.min().values:.3f}")
See Also
- calculate_daily_univariate_statistics() : Compute daily statistics from sub-daily data
- calculate_yearly_monthly_ecdfs() : Compute yearly-monthly empirical CDFs
- calculate_long_term_monthly_ecdfs() : Compute long-term monthly empirical CDFs
- align_and_broadcast() : Align arrays for element-wise operations
Source code in pvgisprototype/algorithms/finkelstein_schafer/statistics.py
@log_function_call
def calculate_finkelstein_schafer_statistics(
location_series_data_array: DataArray | Dataset,
) -> tuple[DataArray, Dataset, DataArray, DataArray]:
"""
Calculate Finkelstein-Schafer statistics for typical meteorological year
selection.
The Finkelstein-Schafer (FS) statistic quantifies how well a candidate
month represents the long-term climate by comparing its cumulative
distribution function (CDF) to the multi-year average CDF. Lower FS values
indicate months that are more representative of typical conditions.
This implementation follows the ISO 15927-4:2005 standard for determining
typical meteorological data and the methodology described in Hall et al.
(1978) [1]_.
Algorithm
---------
The Finkelstein-Schafer statistic is computed through the following steps:
1. **Daily aggregation**: Calculate daily mean values from sub-daily
(e.g., hourly) time series data.
2. **Long-term CDF**: For each calendar month *m*, compute the empirical
cumulative distribution function :math:`\\phi(q, m)` using daily values
aggregated across all years in the dataset.
3. **Yearly CDFs**: For each calendar month *m* and year *y* for a quantity
*q*, compute the empirical cumulative distribution function
:math:`F(q, m, y)` using only the daily values from that specific month
and year.
4. **Finkelstein-Schafer statistic**: For each month *m* and year *y* for a
quantity *q*, calculate the Finkelstein-Schafer statistic by integrating
the absolute difference between the yearly and long-term CDFs:
.. math::
FS(q, m, y) = \\sum_{i} |F(q_i, m, y) - \\phi(q_i, m)|
where the summation is over all quantile values :math:`q_i` in the
distribution.
5. **Ranking**: For each calendar month *m* for the quantity *q*, candidate
months from different years are ranked by increasing Finkelstein-Schafer
statistic FS(q, m, y). Lower values indicate months that better
represent typical conditions.
Parameters
----------
location_series_data_array : DataArray or Dataset
Time series meteorological data with a temporal dimension (typically
hourly or sub-daily resolution). The data should span multiple years to
enable meaningful long-term statistics. Acceptable formats include:
- ``xarray.DataArray``: Single variable time series
- ``xarray.Dataset``: Multi-variable time series
The time coordinate should be datetime-like and parseable by pandas.
Returns
-------
finkelstein_schafer_statistic : xarray.DataArray
The computed Finkelstein-Schafer statistic with dimensions ``(month,
year)``, where:
- ``month``: Calendar month (1-12)
- ``year``: Individual years in the input dataset
Lower values indicate months that better represent long-term typical
conditions.
daily_statistics : xarray.Dataset
Daily aggregated statistics including mean, min, max, and std computed
from the input time series. Contains dimensions ``(time,)`` where time
represents daily timestamps.
yearly_monthly_ecdfs : xarray.DataArray
Empirical cumulative distribution functions for each individual
month-year combination. Dimensions: ``(month, year, quantile)``.
long_term_monthly_ecdfs : xarray.DataArray
Long-term empirical cumulative distribution functions computed across
all years for each calendar month. Dimensions: ``(month, quantile)``.
Notes
-----
The Finkelstein-Schafer statistic is a non-parametric measure that:
- Does **not** assume any particular distribution shape (e.g., normal,
uniform) Accounts for the **entire distribution**, not just central
tendency (mean/median)
- Is robust to outliers and extreme values
- Enables objective selection of typical months based on distributional
similarity
**Typical use case**: This function is the core computational step in
generating Typical Meteorological Year (TMY) datasets. After computing FS
statistics for multiple meteorological variables (temperature, irradiance,
humidity, wind speed), a weighted sum across variables is used to select
the most representative month for each calendar month position in the TMY.
**Data requirements**:
- Minimum 5-10 years of data recommended for stable statistics
- Complete or near-complete temporal coverage (missing data impacts CDF
accuracy)
- Sub-daily resolution (hourly preferred) to capture diurnal patterns
References
----------
.. [1] Hall, I. J., Prairie, R. R., Anderson, H. E., & Boes, E. C. (1978).
"Generation of a Typical Meteorological Year." Proceedings of the
1978 Annual Meeting of the American Section of the International
Solar Energy Society (AS/ISES), Denver, CO.
.. [2] ISO 15927-4:2005. "Hygrothermal performance of buildings -
Calculation and presentation of climatic data - Part 4: Hourly data
for assessing the annual energy use for heating and cooling."
.. [3] Wilcox, S., & Marion, W. (2008). "Users Manual for TMY3 Data Sets."
Technical Report NREL/TP-581-43156, National Renewable Energy
Laboratory.
Examples
--------
>>> import xarray as xr
>>> import numpy as np
>>> import pandas as pd
>>> from pvgisprototype.algorithms.finkelstein_schafer.statistics import (
... calculate_finkelstein_schafer_statistics
... )
>>>
>>> # Create sample hourly temperature data for 3 years
>>> time = pd.date_range('2015-01-01', '2017-12-31 23:00', freq='H')
>>> temperature = 15 + 10 * np.sin(2 * np.pi * np.arange(len(time)) / (365.25 * 24))
>>> temperature += np.random.normal(0, 2, len(time)) # Add noise
>>>
>>> data = xr.DataArray(
... temperature,
... coords={'time': time},
... dims=['time'],
... name='temperature'
... )
>>>
>>> # Calculate Finkelstein-Schafer statistics
>>> fs_statistics, daily_statistics, yearly_ecdfs, long_term_ecdfs = (
... calculate_finkelstein_schafer_statistics(data)
... )
>>>
>>> # Find the most representative January across all years
>>> january_fs = fs_stat.sel(month=1)
>>> best_january_year = january_fs.idxmin(dim='year').values
>>> print(f"Most typical January: {best_january_year}") # doctest: +SKIP
>>> print(f"FS statistic: {january_fs.min().values:.3f}") # doctest: +SKIP
See Also
--------
- calculate_daily_univariate_statistics() : Compute daily statistics from sub-daily data
- calculate_yearly_monthly_ecdfs() : Compute yearly-monthly empirical CDFs
- calculate_long_term_monthly_ecdfs() : Compute long-term monthly empirical CDFs
- align_and_broadcast() : Align arrays for element-wise operations
"""
# 1. Calculate daily means from hourly values
daily_statistics = calculate_daily_univariate_statistics(
data_array=location_series_data_array,
)
# 2. Calculate yearly-monthly ECDFs: F(q, m, y)
yearly_monthly_ecdfs = calculate_yearly_monthly_ecdfs(
dataset=daily_statistics,
variable="mean",
)
# 3. Calculate long-term monthly ECDFs: φ(q, m)
long_term_monthly_ecdfs = calculate_long_term_monthly_ecdfs(
dataset=daily_statistics,
variable="mean",
)
# 4. Align long-term ECDFs to yearly-monthly structure for subtraction
aligned_long_term_monthly_ecdfs = align_and_broadcast(
long_term_monthly_ecdfs, yearly_monthly_ecdfs
)
# 5. Calculate FS statistic: ∑|F(q,m,y) - φ(q,m)|
finkelstein_schafer_statistic = abs(
yearly_monthly_ecdfs - aligned_long_term_monthly_ecdfs
).sum(dim="quantile")
return (
finkelstein_schafer_statistic,
daily_statistics,
yearly_monthly_ecdfs,
long_term_monthly_ecdfs,
)
univariate_statistics ¶
Functions:
| Name | Description |
|---|---|
calculate_daily_univariate_statistics | Calculate daily maximum, minimum, and mean for each variable in the dataset using pandas. |
calculate_daily_univariate_statistics ¶
Calculate daily maximum, minimum, and mean for each variable in the dataset using pandas. Preserves latitude (lat) and longitude (lon) coordinates of the original data.
Source code in pvgisprototype/algorithms/finkelstein_schafer/univariate_statistics.py
@log_function_call
def calculate_daily_univariate_statistics(
data_array: DataArray,
) -> Dataset:
"""
Calculate daily maximum, minimum, and mean for each variable in the dataset using pandas.
Preserves latitude (lat) and longitude (lon) coordinates of the original data.
"""
# Convert xarray DataArray to pandas DataFrame and resample to daily frequency and compute statistics
daily_statistics = (
data_array.to_dataframe(name="value").resample("1D").agg(["max", "min", "mean"])
)
# Extract lat/lon from the original data_array
lat = data_array.coords["lat"].values if "lat" in data_array.coords else None
lon = data_array.coords["lon"].values if "lon" in data_array.coords else None
# Convert pandas DataFrame back to xarray Dataset
result = Dataset(
{
"max": (["time"], daily_statistics["value"]["max"].values),
"min": (["time"], daily_statistics["value"]["min"].values),
"mean": (["time"], daily_statistics["value"]["mean"].values),
},
coords={
"lon": lon,
"lat": lat,
"time": daily_statistics.index,
},
)
return result
hargreaves ¶
Modules:
| Name | Description |
|---|---|
solar_declination | |
solar_declination ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_declination_series_hargreaves | Approximate the solar declination for a series of timestamps |
calculate_solar_declination_series_hargreaves ¶
calculate_solar_declination_series_hargreaves(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarDeclination
Approximate the solar declination for a series of timestamps using the Hargreaves formula, with days-in-year from each timestamp's year.
⎛360 ⎛ ⎛360 ⎞⎞⎞
δ = 23.45° ⋅ sin ⎜─── ⋅ ⎜284 + n + 0.4 ⋅ sin ⎜─── ⋅ (n - 100)⎟⎟⎟ ⎝365 ⎝ ⎝365 ⎠⎠⎠
Notes
-
365.25: The number 365.25 represents the average number of days in a year. This value is used to scale the orbital position of the Earth.
-
284: The number 284 represents a constant term added to the day of the year. It adjusts the calculation to align with the Earth's position during the winter solstice, which usually occurs around December 21st (approximately 284 days into the year).
-
0.4: The number 0.4 is a constant that determines the amplitude of the seasonal variation. It is multiplied by the second sine term to modulate the seasonal change in the solar declination.
-
100: The number 100 represents an offset to the day of the year. It is subtracted from the original day of the year before calculating the second sine term. This offset helps adjust the timing of the seasonal variation and is usually chosen to align with the summer solstice, which typically occurs around June 21st.
Source code in pvgisprototype/algorithms/hargreaves/solar_declination.py
@log_function_call
@custom_cached
def calculate_solar_declination_series_hargreaves(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarDeclination:
"""
Approximate the solar declination for a series of timestamps
using the Hargreaves formula, with days-in-year from each timestamp's year.
⎛360 ⎛ ⎛360 ⎞⎞⎞
δ = 23.45° ⋅ sin ⎜─── ⋅ ⎜284 + n + 0.4 ⋅ sin ⎜─── ⋅ (n - 100)⎟⎟⎟
⎝365 ⎝ ⎝365 ⎠⎠⎠
Notes
-----
- 365.25: The number 365.25 represents the average number of days in a
year. This value is used to scale the orbital position of the Earth.
- 284: The number 284 represents a constant term added to the day of the
year. It adjusts the calculation to align with the Earth's position
during the winter solstice, which usually occurs around December 21st
(approximately 284 days into the year).
- 0.4: The number 0.4 is a constant that determines the amplitude of the
seasonal variation. It is multiplied by the second sine term to
modulate the seasonal change in the solar declination.
- 100: The number 100 represents an offset to the day of the year. It is
subtracted from the original day of the year before calculating the
second sine term. This offset helps adjust the timing of the seasonal
variation and is usually chosen to align with the summer solstice,
which typically occurs around June 21st.
"""
if not isinstance(timestamps, DatetimeIndex):
raise TypeError("timestamps must be a pandas.DatetimeIndex")
days_in_years = get_days_in_years(timestamps.year)
days_of_year = timestamps.dayofyear
inner_angle = numpy.array(360.0 / days_in_years * (days_of_year - 100.0))
solar_declination_series = numpy.sin(
numpy.radians(
360.0
/ days_in_years
* (284.0 + days_of_year + 0.4 * numpy.sin(numpy.radians(inner_angle)))
)
).to_numpy().astype(dtype)
out_of_range, out_of_range_index = identify_values_out_of_range_x(
series=solar_declination_series,
shape=timestamps.shape,
data_model=SolarDeclination(unit=RADIANS),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_declination_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarDeclination(
value=solar_declination_series,
unit=RADIANS,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
algorithm=SolarDeclinationModel.hargreaves,
)
hofierka ¶
Modules:
| Name | Description |
|---|---|
irradiance | |
position | |
power | |
read | |
spectrum | |
irradiance ¶
Modules:
| Name | Description |
|---|---|
diffuse | |
direct | |
extraterrestrial | |
shortwave | |
diffuse ¶
Modules:
| Name | Description |
|---|---|
clear_sky | |
ground_reflected | |
horizontal | |
clear_sky ¶
Modules:
| Name | Description |
|---|---|
altitude | Diffuse Solar Altitude Function |
ground_reflected | |
horizontal | |
transmission_function | |
altitude ¶
Diffuse Solar Altitude Function
This module implements the diffuse solar altitude function and associated coefficients from Hofierka's clear-sky radiation model (2002). These functions characterize how diffuse irradiance varies with solar elevation under cloudless atmospheric conditions.
Theoretical Background
Under cloudless skies, atmospheric turbidity affects the partitioning of solar radiation into direct and diffuse components. As turbidity increases, diffuse irradiance increases while direct irradiance decreases.
See Also
pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.transmission_function Implements Tₙ(T_LK) diffuse transmission function pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.horizontal Combines transmission and altitude functions for total diffuse irradiance
Functions:
| Name | Description |
|---|---|
calculate_diffuse_solar_altitude_coefficients_series_hofierka | Calculate the diffuse solar altitude coefficients. |
calculate_diffuse_solar_altitude_function_series_hofierka | Calculate the diffuse solar altitude function. |
calculate_diffuse_solar_altitude_coefficients_series_hofierka ¶calculate_diffuse_solar_altitude_coefficients_series_hofierka(
linke_turbidity_factor_series: LinkeTurbidityFactor,
verbose: int = 0,
log: int = 0,
) -> tuple
Calculate the diffuse solar altitude coefficients.
Calculates the diffuse solar altitude coefficients (a1, a2, a3) over a time series required for calculating the diffuse solar altitude function in Hofierka's model.
These coefficients are derived from the Linke Turbidity factor and the diffuse transmission function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linke_turbidity_factor_series | LinkeTurbidityFactor | The Linke turbidity factors as a list of LinkeTurbidityFactor objects or a single object. | required |
verbose | int | Verbosity level for debugging output. When set to | 0 |
log | int | Logging level for data fingerprinting. When exceeding | 0 |
Returns:
| Type | Description |
|---|---|
tuple of numpy.ndarray | Three arrays containing the coefficient series:
|
Notes
The coefficients are calculated using empirical formulas from Hofierka's model :
- a1 is constrained to ensure
a1 * Td >= 0.0022where Td is diffuse transmission - a2 and a3 are derived from quadratic functions of the Linke turbidity factor
See Also
calculate_diffuse_transmission_function_series_hofierka : Computes diffuse transmission calculate_diffuse_solar_altitude_function_series_hofierka : Uses these coefficients
References
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open source GIS: implementation and applications. Proceedings of the Open source GIS - GRASS users conference, Trento, Italy.
Examples:
>>> from pvgisprototype import LinkeTurbidityFactor
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.altitude import calculate_diffuse_solar_altitude_function_series_hofierka
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.transmission_function import calculate_diffuse_transmission_function_series_hofierka
>>> ltf = LinkeTurbidityFactor(value=np.array([2.0, 2.5, 3.0]))
>>> a1, a2, a3 = calculate_diffuse_solar_altitude_coefficients_series_hofierka(ltf)
>>> print(a1.shape)
(3,)
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/clear_sky/altitude.py
@log_function_call
@custom_cached
def calculate_diffuse_solar_altitude_coefficients_series_hofierka(
# linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
linke_turbidity_factor_series: LinkeTurbidityFactor,
verbose: int = 0,
log: int = 0,
) -> tuple:
"""Calculate the diffuse solar altitude coefficients.
Calculates the diffuse solar altitude coefficients (a1, a2, a3) over a time
series required for calculating the diffuse solar altitude function in
Hofierka's model.
These coefficients are derived from the Linke Turbidity factor and the
diffuse transmission function.
Parameters
----------
linke_turbidity_factor_series: (List[LinkeTurbidityFactor] or LinkeTurbidityFactor)
The Linke turbidity factors as a list of LinkeTurbidityFactor objects
or a single object.
verbose : int, optional
Verbosity level for debugging output. When set to
`DEBUG_AFTER_THIS_VERBOSITY_LEVEL`, prints local variables.
Default is 0.
log : int, optional
Logging level for data fingerprinting. When exceeding
`HASH_AFTER_THIS_VERBOSITY_LEVEL`, logs data hashes for traceability.
Default is 0.
Returns
-------
tuple of numpy.ndarray
Three arrays containing the coefficient series:
- a1_series : First coefficient, bounded by diffuse transmission
- a2_series : Second coefficient
- a3_series : Third coefficient
Notes
-----
The coefficients are calculated using empirical formulas from Hofierka's
model :
- a1 is constrained to ensure `a1 * Td >= 0.0022` where Td is diffuse
transmission
- a2 and a3 are derived from quadratic functions of the Linke turbidity
factor
See Also
--------
calculate_diffuse_transmission_function_series_hofierka : Computes diffuse
transmission
calculate_diffuse_solar_altitude_function_series_hofierka : Uses these
coefficients
References
----------
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open
source GIS: implementation and applications. *Proceedings of the
Open source GIS - GRASS users conference*, Trento, Italy.
Examples
--------
>>> from pvgisprototype import LinkeTurbidityFactor
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.altitude import calculate_diffuse_solar_altitude_function_series_hofierka
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.transmission_function import calculate_diffuse_transmission_function_series_hofierka
>>> ltf = LinkeTurbidityFactor(value=np.array([2.0, 2.5, 3.0]))
>>> a1, a2, a3 = calculate_diffuse_solar_altitude_coefficients_series_hofierka(ltf)
>>> print(a1.shape)
(3,)
"""
linke_turbidity_factor_series_squared_array = np.power(
linke_turbidity_factor_series.value, 2
)
diffuse_transmission_series = calculate_diffuse_transmission_function_series_hofierka(
linke_turbidity_factor_series
)
diffuse_transmission_series_array = np.array(diffuse_transmission_series)
a1_prime_series = (
0.26463
- 0.061581 * linke_turbidity_factor_series.value
+ 0.0031408 * linke_turbidity_factor_series_squared_array
)
a1_series = np.where(
a1_prime_series * diffuse_transmission_series < 0.0022,
# 0.0022 / diffuse_transmission_series_array,
np.maximum(0.0022 / diffuse_transmission_series_array, a1_prime_series),
a1_prime_series,
)
a2_series = (
2.04020
+ 0.018945 * linke_turbidity_factor_series.value
- 0.011161 * linke_turbidity_factor_series_squared_array
)
a3_series = (
-1.3025
+ 0.039231 * linke_turbidity_factor_series.value
+ 0.0085079 * linke_turbidity_factor_series_squared_array
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=(a1_series, a2_series, a3_series),
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return a1_series, a2_series, a3_series
calculate_diffuse_solar_altitude_function_series_hofierka ¶calculate_diffuse_solar_altitude_function_series_hofierka(
solar_altitude_series: SolarAltitude,
linke_turbidity_factor_series: LinkeTurbidityFactor,
verbose: int = 0,
log: int = 0,
) -> ndarray
Calculate the diffuse solar altitude function.
Calculates the diffuse solar altitude function (Fd) for a time series using Hofierka's empirical model. This function combines solar altitude angles with turbidity-dependent coefficients to estimate diffuse irradiance patterns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solar_altitude_series | SolarAltitude | Solar altitude angles for the time series. Must contain | required |
linke_turbidity_factor_series | LinkeTurbidityFactor | Linke turbidity factor values corresponding to the solar altitude time points. | required |
verbose | int | Verbosity level for debugging output. When set to | 0 |
log | int | Logging level for data fingerprinting. When exceeding | 0 |
Returns:
| Type | Description |
|---|---|
ndarray | Diffuse solar altitude function values (Fd) for each time point in the series. Array shape matches input series dimensions. |
Notes
The diffuse solar altitude function Fₐ(Solar Altitude) characterizes the angular distribution of diffuse radiation :
.. math:: F_d(Solar Altitude) = a_1 + a_2 \sin(Solar Altitude) + a_3 \sin^2(Solar Altitude)
where:
- :math:
a_1, a_2, a_3are coefficients derived from Linke turbidity, the a₁ coefficient is bounded to ensure physical validity when multiplied by the transmission function.
See Also
calculate_diffuse_solar_altitude_coefficients_series_hofierka : Computes the a1, a2, a3 coefficients SolarAltitude : Solar altitude data model LinkeTurbidityFactor : Linke turbidity factor data model
References
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open source GIS: implementation and applications. Proceedings of the Open source GIS - GRASS users conference, Trento, Italy.
Examples:
>>> from pvgisprototype import SolarAltitude, LinkeTurbidityFactor
>>> import numpy as np
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.altitude import (
>>> calculate_diffuse_solar_altitude_coefficients_series_hofierka,
>>> calculate_diffuse_solar_altitude_function_series_hofierka,
>>> )
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.transmission_function import calculate_diffuse_transmission_function_series_hofierka
>>> altitude = SolarAltitude(value=np.array([0.5, 0.8, 1.0]), unit='radians')
>>> ltf = LinkeTurbidityFactor(value=np.array([2.0, 2.5, 3.0]))
>>> fd = calculate_diffuse_solar_altitude_function_series_hofierka(altitude, ltf)
>>> print(fd.shape)
(3,)
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/clear_sky/altitude.py
@log_function_call
@custom_cached
def calculate_diffuse_solar_altitude_function_series_hofierka(
solar_altitude_series: SolarAltitude,
# linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
linke_turbidity_factor_series: LinkeTurbidityFactor,
verbose: int = 0,
log: int = 0,
) -> np.ndarray:
"""Calculate the diffuse solar altitude function.
Calculates the diffuse solar altitude function (Fd) for a time series using
Hofierka's empirical model. This function combines solar altitude angles
with turbidity-dependent coefficients to estimate diffuse irradiance
patterns.
Parameters
----------
solar_altitude_series : SolarAltitude
Solar altitude angles for the time series. Must contain `radians` attribute
with angle values in radians.
linke_turbidity_factor_series : LinkeTurbidityFactor
Linke turbidity factor values corresponding to the solar altitude time points.
verbose : int, optional
Verbosity level for debugging output. When set to
`DEBUG_AFTER_THIS_VERBOSITY_LEVEL`, prints local variables.
Default is 0.
log : int, optional
Logging level for data fingerprinting. When exceeding
`HASH_AFTER_THIS_VERBOSITY_LEVEL`, logs data hashes for traceability.
Default is 0.
Returns
-------
numpy.ndarray
Diffuse solar altitude function values (Fd) for each time point in the series.
Array shape matches input series dimensions.
Notes
-----
The diffuse solar altitude function Fₐ(Solar Altitude) characterizes the
angular distribution of diffuse radiation :
.. math::
F_d(Solar Altitude) = a_1 + a_2 \sin(Solar Altitude) + a_3 \sin^2(Solar Altitude)
where:
- :math:`a_1, a_2, a_3` are coefficients derived from Linke turbidity, the
a₁ coefficient is bounded to ensure physical validity when multiplied by
the transmission function.
See Also
--------
calculate_diffuse_solar_altitude_coefficients_series_hofierka : Computes
the a1, a2, a3 coefficients
SolarAltitude : Solar altitude data model
LinkeTurbidityFactor : Linke turbidity factor data model
References
----------
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open
source GIS: implementation and applications. *Proceedings of the
Open source GIS - GRASS users conference*, Trento, Italy.
Examples
--------
>>> from pvgisprototype import SolarAltitude, LinkeTurbidityFactor
>>> import numpy as np
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.altitude import (
>>> calculate_diffuse_solar_altitude_coefficients_series_hofierka,
>>> calculate_diffuse_solar_altitude_function_series_hofierka,
>>> )
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.transmission_function import calculate_diffuse_transmission_function_series_hofierka
>>> altitude = SolarAltitude(value=np.array([0.5, 0.8, 1.0]), unit='radians')
>>> ltf = LinkeTurbidityFactor(value=np.array([2.0, 2.5, 3.0]))
>>> fd = calculate_diffuse_solar_altitude_function_series_hofierka(altitude, ltf)
>>> print(fd.shape)
(3,)
"""
a1_series, a2_series, a3_series = (
calculate_diffuse_solar_altitude_coefficients_series_hofierka(
linke_turbidity_factor_series,
verbose=verbose,
)
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=(a1_series, a2_series, a3_series),
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return (
a1_series
+ a2_series * np.sin(solar_altitude_series.radians)
+ a3_series * np.power(np.sin(solar_altitude_series.radians), 2)
)
ground_reflected ¶
Functions:
| Name | Description |
|---|---|
calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis | Calculate the clear-sky ground-reflected diffuse irradiance on an inclined |
calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis ¶calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
longitude: float,
latitude: float,
elevation: float,
timestamps: DatetimeIndex = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo = ZoneInfo("UTC"),
surface_orientation: float = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: float = SURFACE_TILT_DEFAULT,
surface_tilt_threshold=SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
albedo: float | None = ALBEDO_DEFAULT,
solar_position_model: SolarPositionModel = noaa,
solar_time_model: SolarTimeModel = noaa,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> ClearSkyDiffuseGroundReflectedInclinedIrradiance
Calculate the clear-sky ground-reflected diffuse irradiance on an inclined surface.
Calculates the component of diffuse irradiance that reaches an inclined surface after reflection from the ground under clear-sky conditions. This component is proportional to the global horizontal irradiance [Ghc], the solar surface tilt angle, the mean ground albedo [ρg] and a fraction of the ground viewed by an inclined surface [rg(γN)]. The calculation follows the isotropic sky model where ground-reflected radiation is uniformly distributed.
The ground-reflected irradiance is calculated as:
.. math:: I_{gr} = I_g \cdot F_{gv} \cdot \rho
where:
- :math:
I_{gr}is the ground-reflected irradiance on the inclined surface - :math:
I_gis the global horizontal irradiance - :math:
F_{gv}is the ground view fraction: :math:(1 - \cos(\beta)) / 2 - :math:
\betais the surface tilt angle - :math:
\rhois the ground albedo
For near-horizontal surfaces (tilt ≤ threshold), ground reflection is negligible and set to zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | Geographic longitude in decimal degrees. East is positive, west is negative. Valid range: [-180, 180]. | required |
latitude | float | Geographic latitude in decimal degrees. North is positive, south is negative. Valid range: [-90, 90]. | required |
elevation | float | Site elevation above sea level in meters. Used for atmospheric corrections in irradiance calculations. | required |
timestamps | DatetimeIndex | Time series index for calculations. Must be timezone-aware for accurate solar position computations. | DatetimeIndex([now(tz='UTC')]) |
timezone | str | IANA timezone identifier (e.g., 'UTC', 'Europe/Rome'). Used for local time conversions in solar position algorithms. | ZoneInfo('UTC') |
surface_orientation | float | Surface azimuth angle in degrees. 0° = North, 90° = East, 180° = South, 270° = West. Determines horizontal orientation of the inclined surface. | SURFACE_ORIENTATION_DEFAULT |
surface_tilt | float | Surface tilt angle from horizontal in degrees. 0° = horizontal, 90° = vertical. Valid range: [0, 90]. | SURFACE_TILT_DEFAULT |
surface_tilt_threshold | float | Minimum tilt angle in degrees below which ground reflection is considered negligible and set to zero. Default is typically a small value (e.g., 1°). | SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD |
albedo | float | Ground surface albedo (reflectivity), dimensionless. Typical values: 0.2 (grass, soil), 0.3-0.5 (sand, concrete), 0.8 (fresh snow). Valid range: [0, 1]. Default varies by application. | ALBEDO_DEFAULT |
linke_turbidity_factor_series | LinkeTurbidityFactor | Linke turbidity factor values characterizing atmospheric turbidity. Typical range: [1.0, 6.0]. Default is | LinkeTurbidityFactor() |
adjust_for_atmospheric_refraction | bool | Whether to apply atmospheric refraction correction to solar angles. Affects accuracy near horizon. Default is True. | ATMOSPHERIC_REFRACTION_FLAG_DEFAULT |
solar_position_model | str | Algorithm for solar position calculations (e.g., 'NOAA', 'PSA'). Default is | noaa |
solar_time_model | str | Solar time calculation method. Default is | noaa |
solar_constant | float | Solar constant in W·m⁻². Default is | SOLAR_CONSTANT |
eccentricity_phase_offset | float | Phase offset for Earth's orbital eccentricity correction in radians. Default is | ECCENTRICITY_PHASE_OFFSET |
eccentricity_amplitude | float | Amplitude of orbital eccentricity correction. Default is | ECCENTRICITY_CORRECTION_FACTOR |
dtype | str | NumPy data type for array operations. Default is | DATA_TYPE_DEFAULT |
array_backend | str | Array backend ('numpy', 'cupy', 'dask'). Default is | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level for debugging output. When exceeding | VERBOSE_LEVEL_DEFAULT |
log | int | Logging level for data fingerprinting. When exceeding | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
ClearSkyDiffuseGroundReflectedInclinedIrradiance | Data model containing:
|
Notes
Ground View Fraction:
The ground view fraction represents the solid angle subtended by the ground as seen from the tilted surface:
- Horizontal surface (0°): F_gv = 0 (no ground visible)
- Vertical surface (90°): F_gv = 0.5 (half hemisphere is ground)
- Tilted surface: F_gv = (1 - cos(β)) / 2
Tilt Threshold Behavior:
When surface_tilt <= surface_tilt_threshold, the function:
- Sets ground view fraction to 0
- Creates zero-valued arrays for ground reflection
- Avoids unnecessary computation for negligible contributions
This optimization is physically justified as near-horizontal surfaces receive minimal ground-reflected radiation.
Global Horizontal Irradiance
The function internally computes the global horizontal irradiance as:
.. math:: I_g = I_{bh} + I_{dh}
where I_bh is direct horizontal and I_dh is diffuse horizontal irradiance, both calculated using clear-sky models.
Physical Constraints
- Ground-reflected irradiance cannot exceed global horizontal irradiance
- Albedo values outside [0, 1] produce unphysical results
- Results valid only for clear-sky conditions
- Model assumes isotropic ground reflection (Lambertian surface)
See Also
calculate_clear_sky_direct_horizontal_irradiance_series : Computes direct component calculate_clear_sky_diffuse_horizontal_irradiance : Computes diffuse component ClearSkyDiffuseGroundReflectedInclinedIrradiance : Output data model
References
.. [1] Duffie, J. A., & Beckman, W. A. (2013). Solar Engineering of Thermal Processes (4th ed.). Wiley. Chapter 2: Available Solar Radiation. .. [2] Perez, R., et al. (1987). A new simplified version of the Perez diffuse irradiance model for tilted surfaces. Solar Energy, 39(3), 221-231.
Examples:
Calculate ground reflection for a south-facing tilted surface:
>>> import pandas as pd
>>> import numpy as np
>>> from pvgisprototype import Longitude, Latitude
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.ground_reflected import (
... calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis
... )
>>>
>>> # Summer day, hourly data
>>> timestamps = pd.date_range('2024-06-21', periods=24, freq='h', tz='UTC')
>>>
>>> # Location: Rome, Italy
>>> longitude = Longitude(value=12.496, unit='degrees')
>>> latitude = Latitude(value=41.903, unit='degrees')
>>> # Solar surface position
>>> surface_orientation = SurfaceOrientation(value=180, unit='degrees')
>>> surface_tilt = SurfaceTilt(value=180, unit='degrees')
>>> # Calculate irradiance
>>> result = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians, # South-facing
... surface_tilt=surface_tilt.radians,
... albedo=0.2 # Grass/soil
... )
>>>
>>> print(f"Ground view fraction: {result.ground_view_fraction:.3f}")
Ground view fraction: 1.000
>>> print(f"Peak ground-reflected irradiance: {result.value.max():.1f} W/m²")
Peak ground-reflected irradiance: 241.5 W/m²
>>> print(f"Daily total: {result.value.sum():.1f} Wh/m²")
Daily total: 2221.9 Wh/m²
Compare albedo effects
>>> # Low albedo (dark soil)
>>> result_dark = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians,
... surface_tilt=surface_tilt.radians,
... albedo=0.15
... )
>>>
>>> # High albedo (concrete)
>>> result_bright = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians,
... surface_tilt=surface_tilt.radians,
... albedo=0.4
... )
>>>
>>> albedo_ratio = result_bright.value.max() / result_dark.value.max()
>>> print(f"Bright/dark albedo ratio: {albedo_ratio:.2f}")
Bright/dark albedo ratio: 2.67
Near-horizontal surface (negligible ground reflection)
>>> result_flat = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians, # South-facing
... surface_tilt=0.1, # Nearly horizontal
... surface_tilt_threshold=1.0,
... albedo=0.2 # Grass/soil
... )
>>>
>>> print(f"Ground view fraction: {result_flat.ground_view_fraction}")
0.0 # Threshold triggered, no ground reflection
>>> print(f"All values zero: {np.all(result_flat.value == 0)}")
True
Warnings
- Tilt angle must be in degrees, not radians
- Albedo values should be realistic for the surface type
- Model assumes isotropic reflection (not accurate for specular surfaces)
- Clear-sky model; not valid under cloudy conditions
- Input timestamps must be timezone-aware
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/clear_sky/ground_reflected.py
@log_function_call
@custom_cached
def calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
longitude: float,
latitude: float,
elevation: float,
timestamps: DatetimeIndex = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo = ZoneInfo('UTC'),
surface_orientation: float = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: float = SURFACE_TILT_DEFAULT,
surface_tilt_threshold = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
# unrefracted_solar_zenith: UnrefractedSolarZenith | None = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT, # radians
albedo: float | None = ALBEDO_DEFAULT,
solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> ClearSkyDiffuseGroundReflectedInclinedIrradiance:
"""
Calculate the clear-sky ground-reflected diffuse irradiance on an inclined
surface.
Calculates the component of diffuse irradiance that reaches an inclined
surface after reflection from the ground under clear-sky conditions. This
component is proportional to the global horizontal irradiance [Ghc], the
solar surface tilt angle, the mean ground albedo [ρg] and a fraction of the
ground viewed by an inclined surface [rg(γN)]. The calculation follows the
isotropic sky model where ground-reflected radiation is uniformly
distributed.
The ground-reflected irradiance is calculated as:
.. math::
I_{gr} = I_g \cdot F_{gv} \cdot \\rho
where:
- :math:`I_{gr}` is the ground-reflected irradiance on the inclined surface
- :math:`I_g` is the global horizontal irradiance
- :math:`F_{gv}` is the ground view fraction: :math:`(1 - \cos(\\beta)) / 2`
- :math:`\\beta` is the surface tilt angle
- :math:`\\rho` is the ground albedo
For near-horizontal surfaces (tilt ≤ threshold), ground reflection is
negligible and set to zero.
Parameters
----------
longitude : float
Geographic longitude in decimal degrees. East is positive, west is
negative. Valid range: [-180, 180].
latitude : float
Geographic latitude in decimal degrees. North is positive, south is
negative. Valid range: [-90, 90].
elevation : float
Site elevation above sea level in meters. Used for atmospheric
corrections in irradiance calculations.
timestamps : pandas.DatetimeIndex
Time series index for calculations. Must be timezone-aware for
accurate solar position computations.
timezone : str
IANA timezone identifier (e.g., 'UTC', 'Europe/Rome'). Used for
local time conversions in solar position algorithms.
surface_orientation : float
Surface azimuth angle in degrees. 0° = North, 90° = East, 180° = South,
270° = West. Determines horizontal orientation of the inclined surface.
surface_tilt : float
Surface tilt angle from horizontal in degrees. 0° = horizontal,
90° = vertical. Valid range: [0, 90].
surface_tilt_threshold : float, optional
Minimum tilt angle in degrees below which ground reflection is
considered negligible and set to zero. Default is typically a small
value (e.g., 1°).
albedo : float, optional
Ground surface albedo (reflectivity), dimensionless. Typical values:
0.2 (grass, soil), 0.3-0.5 (sand, concrete), 0.8 (fresh snow).
Valid range: [0, 1]. Default varies by application.
linke_turbidity_factor_series : LinkeTurbidityFactor, optional
Linke turbidity factor values characterizing atmospheric turbidity.
Typical range: [1.0, 6.0]. Default is
``LinkeTurbidityFactor()``.
adjust_for_atmospheric_refraction : bool, optional
Whether to apply atmospheric refraction correction to solar angles.
Affects accuracy near horizon. Default is True.
solar_position_model : str, optional
Algorithm for solar position calculations (e.g., 'NOAA', 'PSA').
Default is ``SOLAR_POSITION_MODEL_DEFAULT``.
solar_time_model : str, optional
Solar time calculation method. Default is ``SOLAR_TIME_MODEL_DEFAULT``.
solar_constant : float, optional
Solar constant in W·m⁻². Default is ``SOLAR_CONSTANT`` (~1361 W·m⁻²).
eccentricity_phase_offset : float, optional
Phase offset for Earth's orbital eccentricity correction in radians.
Default is ``ECCENTRICITY_PHASE_OFFSET``.
eccentricity_amplitude : float, optional
Amplitude of orbital eccentricity correction. Default is
``ECCENTRICITY_CORRECTION_FACTOR``.
dtype : str, optional
NumPy data type for array operations. Default is ``DATA_TYPE_DEFAULT``.
array_backend : str, optional
Array backend ('numpy', 'cupy', 'dask'). Default is
``ARRAY_BACKEND_DEFAULT``.
verbose : int, optional
Verbosity level for debugging output. When exceeding
``DEBUG_AFTER_THIS_VERBOSITY_LEVEL``, prints all local variables
using devtools.debug. Default is ``VERBOSE_LEVEL_DEFAULT``.
log : int, optional
Logging level for data fingerprinting. When exceeding
``HASH_AFTER_THIS_VERBOSITY_LEVEL``, logs data hashes for
reproducibility tracking. Default is ``LOG_LEVEL_DEFAULT``.
Returns
-------
ClearSkyDiffuseGroundReflectedInclinedIrradiance
Data model containing:
- **value** : numpy.ndarray
Ground-reflected irradiance values in W·m⁻² for each timestamp
- **out_of_range** : bool
Flag indicating if any values exceed physical bounds
- **out_of_range_index** : numpy.ndarray
Indices of out-of-range values
- **ground_view_fraction** : float
Fraction of ground visible from tilted surface (0 to 0.5)
- **albedo** : float
Ground albedo used in calculation
- **global_horizontal_irradiance** : numpy.ndarray
Global horizontal irradiance values used
- **direct_horizontal_irradiance** : array or None
Direct component of horizontal irradiance
- **diffuse_horizontal_irradiance** : array or None
Diffuse component of horizontal irradiance
- **solar_positioning_algorithm** : str
Solar position algorithm used
- **solar_timing_algorithm** : str
Solar time algorithm used
- **location** : tuple
(longitude, latitude) coordinates
- **elevation** : float
Site elevation in meters
- **surface_orientation** : float
Surface azimuth in degrees
- **surface_tilt** : float
Surface tilt in degrees
- **surface_tilt_threshold** : float
Tilt threshold used
Notes
-----
**Ground View Fraction:**
The ground view fraction represents the solid angle subtended by the ground
as seen from the tilted surface:
- Horizontal surface (0°): F_gv = 0 (no ground visible)
- Vertical surface (90°): F_gv = 0.5 (half hemisphere is ground)
- Tilted surface: F_gv = (1 - cos(β)) / 2
**Tilt Threshold Behavior:**
When ``surface_tilt <= surface_tilt_threshold``, the function:
1. Sets ground view fraction to 0
2. Creates zero-valued arrays for ground reflection
3. Avoids unnecessary computation for negligible contributions
This optimization is physically justified as near-horizontal surfaces
receive minimal ground-reflected radiation.
**Global Horizontal Irradiance**
The function internally computes the global horizontal irradiance as:
.. math::
I_g = I_{bh} + I_{dh}
where I_bh is direct horizontal and I_dh is diffuse horizontal irradiance,
both calculated using clear-sky models.
**Physical Constraints**
- Ground-reflected irradiance cannot exceed global horizontal irradiance
- Albedo values outside [0, 1] produce unphysical results
- Results valid only for clear-sky conditions
- Model assumes isotropic ground reflection (Lambertian surface)
See Also
--------
calculate_clear_sky_direct_horizontal_irradiance_series : Computes direct component
calculate_clear_sky_diffuse_horizontal_irradiance : Computes diffuse component
ClearSkyDiffuseGroundReflectedInclinedIrradiance : Output data model
References
----------
.. [1] Duffie, J. A., & Beckman, W. A. (2013). *Solar Engineering of Thermal
Processes* (4th ed.). Wiley. Chapter 2: Available Solar Radiation.
.. [2] Perez, R., et al. (1987). A new simplified version of the Perez diffuse
irradiance model for tilted surfaces. *Solar Energy*, 39(3), 221-231.
Examples
--------
Calculate ground reflection for a south-facing tilted surface:
>>> import pandas as pd
>>> import numpy as np
>>> from pvgisprototype import Longitude, Latitude
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.ground_reflected import (
... calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis
... )
>>>
>>> # Summer day, hourly data
>>> timestamps = pd.date_range('2024-06-21', periods=24, freq='h', tz='UTC')
>>>
>>> # Location: Rome, Italy
>>> longitude = Longitude(value=12.496, unit='degrees')
>>> latitude = Latitude(value=41.903, unit='degrees')
>>> # Solar surface position
>>> surface_orientation = SurfaceOrientation(value=180, unit='degrees')
>>> surface_tilt = SurfaceTilt(value=180, unit='degrees')
>>> # Calculate irradiance
>>> result = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians, # South-facing
... surface_tilt=surface_tilt.radians,
... albedo=0.2 # Grass/soil
... )
>>>
>>> print(f"Ground view fraction: {result.ground_view_fraction:.3f}")
Ground view fraction: 1.000
>>> print(f"Peak ground-reflected irradiance: {result.value.max():.1f} W/m²")
Peak ground-reflected irradiance: 241.5 W/m²
>>> print(f"Daily total: {result.value.sum():.1f} Wh/m²")
Daily total: 2221.9 Wh/m²
Compare albedo effects
>>> # Low albedo (dark soil)
>>> result_dark = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians,
... surface_tilt=surface_tilt.radians,
... albedo=0.15
... )
>>>
>>> # High albedo (concrete)
>>> result_bright = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians,
... surface_tilt=surface_tilt.radians,
... albedo=0.4
... )
>>>
>>> albedo_ratio = result_bright.value.max() / result_dark.value.max()
>>> print(f"Bright/dark albedo ratio: {albedo_ratio:.2f}")
Bright/dark albedo ratio: 2.67
Near-horizontal surface (negligible ground reflection)
>>> result_flat = calculate_clear_sky_ground_reflected_inclined_irradiance_series_pvgis(
... longitude=longitude.radians,
... latitude=latitude.radians,
... elevation=20,
... timestamps=timestamps,
... timezone='Europe/Rome',
... surface_orientation=surface_orientation.radians, # South-facing
... surface_tilt=0.1, # Nearly horizontal
... surface_tilt_threshold=1.0,
... albedo=0.2 # Grass/soil
... )
>>>
>>> print(f"Ground view fraction: {result_flat.ground_view_fraction}")
0.0 # Threshold triggered, no ground reflection
>>> print(f"All values zero: {np.all(result_flat.value == 0)}")
True
Warnings
--------
- Tilt angle must be in degrees, not radians
- Albedo values should be realistic for the surface type
- Model assumes isotropic reflection (not accurate for specular surfaces)
- Clear-sky model; not valid under cloudy conditions
- Input timestamps must be timezone-aware
"""
# build reusable parameter dictionaries
coordinates = {
'longitude': longitude,
'latitude': latitude,
}
time = {
'timestamps': timestamps,
'timezone': timezone,
}
solar_positioning = {
'solar_position_model': solar_position_model,
'adjust_for_atmospheric_refraction': adjust_for_atmospheric_refraction,
'solar_time_model': solar_time_model,
}
earth_orbit = {
'eccentricity_phase_offset': eccentricity_phase_offset,
'eccentricity_amplitude': eccentricity_amplitude,
}
array_parameters = {
"dtype": dtype,
"array_backend": array_backend,
}
output_parameters = {
'verbose': verbose, # Is this wanted here ? i.e. not setting = 0 ?
'log': log,
}
if surface_tilt <= surface_tilt_threshold: # No ground reflection for a flat or nearly flat surface
ground_view_fraction = 0
# in order to avoid 'NameError's
flat_surface_array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from timestamps
global_horizontal_irradiance_series = create_array(**flat_surface_array_parameters)
else:
ground_view_fraction = (1 - cos(surface_tilt)) / 2
direct_horizontal_irradiance_series = calculate_clear_sky_direct_horizontal_irradiance_series(
**coordinates,
elevation=elevation,
**time,
linke_turbidity_factor_series=linke_turbidity_factor_series,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
**earth_orbit,
**array_parameters,
**output_parameters,
)
diffuse_horizontal_irradiance_series = calculate_clear_sky_diffuse_horizontal_irradiance(
**coordinates,
**time,
linke_turbidity_factor_series=linke_turbidity_factor_series,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
**solar_positioning,
solar_constant=solar_constant,
**earth_orbit,
**array_parameters,
**output_parameters,
)
global_horizontal_irradiance_series = (
direct_horizontal_irradiance_series.value
+ diffuse_horizontal_irradiance_series.value
)
# --------------------------------------------------------------------
# At this point, the `global_horizontal_irradiance_series` is either :
# _read_ from external time series Or simulated
# --------------------------------------------------------------------
ground_reflected_inclined_irradiance_series = asarray(
global_horizontal_irradiance_series * ground_view_fraction * albedo,
dtype=dtype
).reshape(-1)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=ground_reflected_inclined_irradiance_series,
shape=timestamps.shape,
data_model=ClearSkyDiffuseGroundReflectedInclinedIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=ground_reflected_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
# Common output structure
function_output = {
"value": ground_reflected_inclined_irradiance_series,
"out_of_range": out_of_range,
"out_of_range_index": out_of_range_index,
"ground_view_fraction": ground_view_fraction,
"albedo": albedo,
"global_horizontal_irradiance": global_horizontal_irradiance_series,
"direct_horizontal_irradiance": (
direct_horizontal_irradiance_series
if "direct_horizontal_irradiance_series" in locals()
else None
),
"diffuse_horizontal_irradiance": (
diffuse_horizontal_irradiance_series
if "diffuse_horizontal_irradiance_series" in locals()
else None
),
"solar_positioning_algorithm": getattr(
diffuse_horizontal_irradiance_series,
"solar_positioning_algorithm",
None,
),
"solar_timing_algorithm": getattr(
diffuse_horizontal_irradiance_series, "solar_timing_algorithm", None
),
"location": (longitude, latitude),
"elevation": elevation,
"surface_orientation": surface_orientation,
"surface_tilt": surface_tilt,
"surface_tilt_threshold": surface_tilt_threshold,
}
return ClearSkyDiffuseGroundReflectedInclinedIrradiance(**function_output)
horizontal ¶
Functions:
| Name | Description |
|---|---|
calculate_clear_sky_diffuse_horizontal_irradiance_hofierka | Calculate clear-sky diffuse sky-reflected horizontal irradiance using |
calculate_clear_sky_diffuse_horizontal_irradiance_hofierka ¶calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
timestamps: DatetimeIndex,
linke_turbidity_factor_series: LinkeTurbidityFactor,
solar_altitude_series: SolarAltitude | None = None,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedHorizontalIrradiance
Calculate clear-sky diffuse sky-reflected horizontal irradiance using Hofierka's model.
Calculates the diffuse sky-reflected component of solar irradiance on a horizontal surface under cloudless sky conditions using Hofierka's empirical clear-sky model. This implementation follows the theoretical framework where diffuse irradiance is the product of extraterrestrial irradiance, a diffuse transmission function, and a solar altitude function.
The model estimates diffuse horizontal irradiance (Dₕc) as:
.. math:: D_{hc} = G_0 \cdot T_n(T_{LK}) \cdot F_d(h_0)
where G₀ is the extraterrestrial normal irradiance, Tₙ is the diffuse transmission function dependent on Linke turbidity, and Fₐ is the solar altitude function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamps | DatetimeIndex | Time series index for which to calculate diffuse irradiance. Must be timezone-aware for accurate solar position calculations. | required |
linke_turbidity_factor_series | LinkeTurbidityFactor | Linke turbidity factor values for each timestamp, characterizing atmospheric turbidity. Typical values range from 1.0 (clean) to 6.0 (very turbid). Default is set in | required |
solar_altitude_series | SolarAltitude or None | Pre-calculated solar altitude angles in radians. If None, must be calculated externally before calling this function. Negative values (sun below horizon) are automatically set to NaN. Default is None. | None |
solar_constant | float | Solar constant in W·m⁻². Represents the solar irradiance at mean Earth-Sun distance outside the atmosphere. Default is | SOLAR_CONSTANT |
eccentricity_phase_offset | float | Phase offset for Earth's orbital eccentricity correction in radians. Accounts for perihelion timing. Default is | ECCENTRICITY_PHASE_OFFSET |
eccentricity_amplitude | float | Amplitude of Earth's orbital eccentricity correction factor. Adjusts for varying Earth-Sun distance throughout the year. Default is | ECCENTRICITY_CORRECTION_FACTOR |
dtype | str | NumPy data type for array operations (e.g., 'float32', 'float64'). Default is | DATA_TYPE_DEFAULT |
array_backend | str | Array backend to use for computations ('numpy', 'cupy', 'dask'). Default is | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level for debugging output. When exceeding | VERBOSE_LEVEL_DEFAULT |
log | int | Logging level for data fingerprinting. When exceeding | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
DiffuseSkyReflectedHorizontalIrradiance | Data model containing:
|
Notes
Implementation Details
- Negative solar altitudes (sun below horizon) are automatically set to NaN, then converted to zero irradiance
- Out-of-range values are identified and flagged based on physical constraints
- The function is cached for performance optimization
- All calculations are vectorized for efficiency over time series
Physical Validity
- Valid only for cloudless sky conditions
- Results are physically meaningful only when sun is above horizon
- Linke turbidity should be within [1.0, 6.0] range
- Output irradiance cannot exceed extraterrestrial values
Model Limitations
- Assumes horizontally homogeneous atmosphere
- Does not include ground-reflected component
- Model calibrated for mid-latitude conditions (?)
See Also
calculate_diffuse_transmission_function_series_hofierka : Calculates Tₙ(T_LK) calculate_diffuse_solar_altitude_function_series_hofierka : Calculates Fₐ(h₀) calculate_extraterrestrial_normal_irradiance_series : Calculates G₀ pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.altitude : Altitude function module
References
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open source GIS: implementation and applications. Proceedings of the Open source GIS - GRASS users conference, Trento, Italy. .. [2] Rigollier, C., Bauer, O., & Wald, L. (2000). On the clear sky model of the ESRA—European Solar Radiation Atlas—with respect to the Heliosat method. Solar Energy, 68(1), 33-48.
Examples:
Calculate diffuse irradiance for a single day :
>>> import pandas as pd
>>> import numpy as np
>>> from pvgisprototype import SolarAltitude, LinkeTurbidityFactor
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.horizontal import (
... calculate_clear_sky_diffuse_horizontal_irradiance_hofierka
... )
>>>
>>> # Create hourly timestamps for one day
>>> timestamps = pd.date_range(
... '2024-06-21', periods=24, freq='h', tz='UTC'
... )
>>>
>>> # Calculate solar altitude (normally done via solar position function)
>>> # Here using example values for demonstration
>>> altitude_values = np.array([
... -0.5, -0.3, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.1, 1.2, 1.2, 1.15,
... 1.1, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0, -0.2, -0.4, -0.5, -0.6, -0.6
... ])
>>> solar_altitude = SolarAltitude(value=altitude_values, unit='radians')
>>>
>>> # Constant moderate turbidity
>>> turbidity = LinkeTurbidityFactor(value=np.full(24, 3.0))
>>>
>>> # Calculate diffuse irradiance
>>> result = calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
... timestamps=timestamps,
... solar_altitude_series=solar_altitude,
... linke_turbidity_factor_series=turbidity
... )
>>>
>>> # Display results
>>> print(f"Max diffuse irradiance: {result.value.max():.1f} W/m²")
>>>
>>> # Check for out-of-range values
>>> if result.out_of_range is not None:
... print(f"Warning: {len(result.out_of_range_index)} values out of range")
Calculate with time-varying turbidity :
>>> # Simulate varying atmospheric conditions
>>> turbidity_varying = LinkeTurbidityFactor(
... value=np.linspace(2.5, 3.5, 24) # Increasing turbidity
... )
>>>
>>> result_varying = calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
... timestamps=timestamps,
... solar_altitude_series=solar_altitude,
... linke_turbidity_factor_series=turbidity_varying
... )
>>>
>>> print(f"Turbidity effect: {result_varying.value.max() - result.value.max():.1f} W/m²")
Warnings
- Negative solar altitudes are automatically converted to zero irradiance
- Input arrays (timestamps, turbidity, altitude) must have matching dimensions
- Very high turbidity values (>6.0) may produce physically unrealistic results (?)
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/clear_sky/horizontal.py
@log_function_call
@custom_cached
def calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
timestamps: DatetimeIndex,
# linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
linke_turbidity_factor_series: LinkeTurbidityFactor,
solar_altitude_series: SolarAltitude | None = None,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedHorizontalIrradiance:
"""
Calculate clear-sky diffuse sky-reflected horizontal irradiance using
Hofierka's model.
Calculates the diffuse sky-reflected component of solar irradiance on a
horizontal surface under cloudless sky conditions using Hofierka's
empirical clear-sky model. This implementation follows the theoretical
framework where diffuse irradiance is the product of extraterrestrial
irradiance, a diffuse transmission function, and a solar altitude function.
The model estimates diffuse horizontal irradiance (Dₕc) as:
.. math::
D_{hc} = G_0 \cdot T_n(T_{LK}) \cdot F_d(h_0)
where G₀ is the extraterrestrial normal irradiance, Tₙ is the diffuse
transmission function dependent on Linke turbidity, and Fₐ is the solar
altitude function.
Parameters
----------
timestamps : pandas.DatetimeIndex
Time series index for which to calculate diffuse irradiance. Must be
timezone-aware for accurate solar position calculations.
linke_turbidity_factor_series : LinkeTurbidityFactor, optional
Linke turbidity factor values for each timestamp, characterizing
atmospheric turbidity. Typical values range from 1.0 (clean) to 6.0
(very turbid). Default is set in ``LinkeTurbidityFactor()``.
solar_altitude_series : SolarAltitude or None, optional
Pre-calculated solar altitude angles in radians. If None, must be
calculated externally before calling this function. Negative values
(sun below horizon) are automatically set to NaN. Default is None.
solar_constant : float, optional
Solar constant in W·m⁻². Represents the solar irradiance at mean
Earth-Sun distance outside the atmosphere. Default is ``SOLAR_CONSTANT``
(typically 1361 W·m⁻²).
eccentricity_phase_offset : float, optional
Phase offset for Earth's orbital eccentricity correction in radians.
Accounts for perihelion timing. Default is ``ECCENTRICITY_PHASE_OFFSET``.
eccentricity_amplitude : float, optional
Amplitude of Earth's orbital eccentricity correction factor. Adjusts
for varying Earth-Sun distance throughout the year. Default is
``ECCENTRICITY_CORRECTION_FACTOR``.
dtype : str, optional
NumPy data type for array operations (e.g., 'float32', 'float64').
Default is ``DATA_TYPE_DEFAULT``.
array_backend : str, optional
Array backend to use for computations ('numpy', 'cupy', 'dask').
Default is ``ARRAY_BACKEND_DEFAULT``.
verbose : int, optional
Verbosity level for debugging output. When exceeding
``DEBUG_AFTER_THIS_VERBOSITY_LEVEL``, prints all local variables.
Default is ``VERBOSE_LEVEL_DEFAULT``.
log : int, optional
Logging level for data fingerprinting. When exceeding
``HASH_AFTER_THIS_VERBOSITY_LEVEL``, logs data hashes for
reproducibility tracking. Default is ``LOG_LEVEL_DEFAULT``.
Returns
-------
DiffuseSkyReflectedHorizontalIrradiance
Data model containing:
- **value** : numpy.ndarray
Diffuse horizontal irradiance values in W·m⁻² for each timestamp
- **out_of_range** : bool
Flag indicating if any values exceed physical bounds
- **out_of_range_index** : numpy.ndarray
Indices of out-of-range values
- **extraterrestrial_normal_irradiance** : array
Calculated extraterrestrial irradiance values
- **linke_turbidity_factor** : LinkeTurbidityFactor
Input turbidity values used in calculation
- **solar_altitude** : SolarAltitude
Solar altitude angles used in calculation
- **solar_positioning_algorithm** : str
Algorithm used for solar position calculations
- **adjust_for_atmospheric_refraction** : bool
Whether atmospheric refraction correction was applied
Notes
-----
**Implementation Details**
- Negative solar altitudes (sun below horizon) are automatically set to NaN,
then converted to zero irradiance
- Out-of-range values are identified and flagged based on physical constraints
- The function is cached for performance optimization
- All calculations are vectorized for efficiency over time series
**Physical Validity**
- Valid only for cloudless sky conditions
- Results are physically meaningful only when sun is above horizon
- Linke turbidity should be within [1.0, 6.0] range
- Output irradiance cannot exceed extraterrestrial values
**Model Limitations**
- Assumes horizontally homogeneous atmosphere
- Does not include ground-reflected component
- Model calibrated for mid-latitude conditions (?)
See Also
--------
calculate_diffuse_transmission_function_series_hofierka : Calculates Tₙ(T_LK)
calculate_diffuse_solar_altitude_function_series_hofierka : Calculates Fₐ(h₀)
calculate_extraterrestrial_normal_irradiance_series : Calculates G₀
pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.altitude : Altitude function module
References
----------
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open
source GIS: implementation and applications. *Proceedings of the
Open source GIS - GRASS users conference*, Trento, Italy.
.. [2] Rigollier, C., Bauer, O., & Wald, L. (2000). On the clear sky model
of the ESRA—European Solar Radiation Atlas—with respect to the
Heliosat method. *Solar Energy*, 68(1), 33-48.
Examples
--------
Calculate diffuse irradiance for a single day :
>>> import pandas as pd
>>> import numpy as np
>>> from pvgisprototype import SolarAltitude, LinkeTurbidityFactor
>>> from pvgisprototype.algorithms.hofierka.irradiance.diffuse.clear_sky.horizontal import (
... calculate_clear_sky_diffuse_horizontal_irradiance_hofierka
... )
>>>
>>> # Create hourly timestamps for one day
>>> timestamps = pd.date_range(
... '2024-06-21', periods=24, freq='h', tz='UTC'
... )
>>>
>>> # Calculate solar altitude (normally done via solar position function)
>>> # Here using example values for demonstration
>>> altitude_values = np.array([
... -0.5, -0.3, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.1, 1.2, 1.2, 1.15,
... 1.1, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0, -0.2, -0.4, -0.5, -0.6, -0.6
... ])
>>> solar_altitude = SolarAltitude(value=altitude_values, unit='radians')
>>>
>>> # Constant moderate turbidity
>>> turbidity = LinkeTurbidityFactor(value=np.full(24, 3.0))
>>>
>>> # Calculate diffuse irradiance
>>> result = calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
... timestamps=timestamps,
... solar_altitude_series=solar_altitude,
... linke_turbidity_factor_series=turbidity
... )
>>>
>>> # Display results
>>> print(f"Max diffuse irradiance: {result.value.max():.1f} W/m²")
>>> print(f"Daily total: {result.value.sum():.1f} Wh/m²")
>>>
>>> # Check for out-of-range values
>>> if result.out_of_range is not None:
... print(f"Warning: {len(result.out_of_range_index)} values out of range")
Calculate with time-varying turbidity :
>>> # Simulate varying atmospheric conditions
>>> turbidity_varying = LinkeTurbidityFactor(
... value=np.linspace(2.5, 3.5, 24) # Increasing turbidity
... )
>>>
>>> result_varying = calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
... timestamps=timestamps,
... solar_altitude_series=solar_altitude,
... linke_turbidity_factor_series=turbidity_varying
... )
>>>
>>> print(f"Turbidity effect: {result_varying.value.max() - result.value.max():.1f} W/m²")
Warnings
--------
- Negative solar altitudes are automatically converted to zero irradiance
- Input arrays (timestamps, turbidity, altitude) must have matching
dimensions
- Very high turbidity values (>6.0) may produce physically unrealistic
results (?)
"""
extraterrestrial_normal_irradiance_series = (
calculate_extraterrestrial_normal_irradiance_series(
timestamps=timestamps,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
)
# Should this maybe happen already outside this function ? ---------------
# Suppress negative solar altitude, else we get high-negative diffuse output
solar_altitude_series.value[solar_altitude_series.value < 0] = np.nan
# ------------------------------------------------------------------------
diffuse_horizontal_irradiance_series = (
extraterrestrial_normal_irradiance_series.value
* calculate_diffuse_transmission_function_series_hofierka(
linke_turbidity_factor_series=linke_turbidity_factor_series,
verbose=verbose,
)
* calculate_diffuse_solar_altitude_function_series_hofierka(
solar_altitude_series=solar_altitude_series,
linke_turbidity_factor_series=linke_turbidity_factor_series,
verbose=verbose,
)
)
# ------------------------------------------------------------------------
diffuse_horizontal_irradiance_series = np.nan_to_num(
diffuse_horizontal_irradiance_series, nan=0
) # safer ? -------------------------------------------------------------
out_of_range, out_of_range_index = identify_values_out_of_range(
series=diffuse_horizontal_irradiance_series,
shape=timestamps.shape,
data_model=DiffuseSkyReflectedHorizontalIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=diffuse_horizontal_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DiffuseSkyReflectedHorizontalIrradiance(
value=diffuse_horizontal_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
linke_turbidity_factor=linke_turbidity_factor_series,
solar_altitude=solar_altitude_series,
solar_positioning_algorithm=solar_altitude_series.solar_positioning_algorithm,
adjust_for_atmospheric_refraction=solar_altitude_series.adjusted_for_atmospheric_refraction,
)
transmission_function ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_transmission_function_series_hofierka | Diffuse transmission function over a period of time |
calculate_diffuse_transmission_function_series_hofierka ¶calculate_diffuse_transmission_function_series_hofierka(
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
) -> ndarray
Diffuse transmission function over a period of time
The diffuse transmission function Tₙ(T_LK) represents the theoretical diffuse irradiance on a horizontal surface with the sun at zenith, normalized for air mass 2 :
.. math:: T_n(T_{LK}) = -0.015843 + 0.030543 T_{LK} + 0.0003797 T_{LK}^2
This quadratic relationship captures how atmospheric turbidity modulates the transmission of diffuse radiation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linke_turbidity_factor_series | LinkeTurbidityFactor | linke_turbidity_factor_series | LinkeTurbidityFactor() |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | 0 |
log | int | log | 0 |
Returns:
| Type | Description |
|---|---|
ndarray | |
Notes
From Thomas Huld's custom r.pv source code:
tn = -0.015843 + locLinke * (0.030543 + 0.0003797 * locLinke);
From Hofierka (2002) :
The estimate of the transmission function Tn(TLK) gives a theoretical
diffuse irradiance on a horizontal surface with the sun vertically
overhead for the air mass 2 Linke turbidity factor. The following
second order polynomial expression is used:
Tn(TLK) = -0.015843 + 0.030543 TLK + 0.0003797 TLK^2
References
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open source GIS: implementation and applications. Proceedings of the Open source GIS - GRASS users conference, Trento, Italy.
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/clear_sky/transmission_function.py
@log_function_call
@custom_cached
def calculate_diffuse_transmission_function_series_hofierka(
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
) -> ndarray:
"""Diffuse transmission function over a period of time
The diffuse transmission function Tₙ(T_LK) represents the theoretical
diffuse irradiance on a horizontal surface with the sun at zenith,
normalized for air mass 2 :
.. math::
T_n(T_{LK}) = -0.015843 + 0.030543 T_{LK} + 0.0003797 T_{LK}^2
This quadratic relationship captures how atmospheric turbidity modulates the
transmission of diffuse radiation.
Parameters
----------
linke_turbidity_factor_series :
linke_turbidity_factor_series
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
Returns
-------
ndarray
Notes
-----
From Thomas Huld's custom `r.pv` source code:
tn = -0.015843 + locLinke * (0.030543 + 0.0003797 * locLinke);
From Hofierka (2002) :
The estimate of the transmission function Tn(TLK) gives a theoretical
diffuse irradiance on a horizontal surface with the sun vertically
overhead for the air mass 2 Linke turbidity factor. The following
second order polynomial expression is used:
Tn(TLK) = -0.015843 + 0.030543 TLK + 0.0003797 TLK^2
References
----------
.. [1] Hofierka, J., & Šúri, M. (2002). The solar radiation model for Open
source GIS: implementation and applications. *Proceedings of the
Open source GIS - GRASS users conference*, Trento, Italy.
"""
linke_turbidity_factor_series_squared_array = np.power(
linke_turbidity_factor_series.value, 2, dtype=dtype
)
diffuse_transmission_series = (
-0.015843
+ 0.030543 * linke_turbidity_factor_series.value
+ 0.0003797 * linke_turbidity_factor_series_squared_array
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=diffuse_transmission_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return diffuse_transmission_series
ground_reflected ¶
Functions:
| Name | Description |
|---|---|
calculate_ground_reflected_inclined_irradiance_series_pvgis | Calculate the clear-sky diffuse ground reflected irradiance on an inclined surface (Ri). |
calculate_ground_reflected_inclined_irradiance_series_pvgis ¶
calculate_ground_reflected_inclined_irradiance_series_pvgis(
longitude: float,
latitude: float,
elevation: float,
timestamps: DatetimeIndex = DatetimeIndex(
[now(tz="UTC")]
),
surface_orientation: float = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: float = SURFACE_TILT_DEFAULT,
surface_tilt_threshold=SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
albedo: float | None = ALBEDO_DEFAULT,
global_horizontal_irradiance: ndarray | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
)
Calculate the clear-sky diffuse ground reflected irradiance on an inclined surface (Ri).
The calculation assumes isotropy. The ground reflected clear-sky irradiance received on an inclined surface [W.m-2] is proportional to the global horizontal irradiance Ghc, to the mean ground albedo ρg and a fraction of the ground viewed by an inclined surface rg(γN).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | longitude | required |
latitude | float | latitude | required |
elevation | float | elevation | required |
timestamps | DatetimeIndex | timestamps | DatetimeIndex([now(tz='UTC')]) |
surface_orientation | float | surface_orientation | SURFACE_ORIENTATION_DEFAULT |
surface_tilt | float | surface_tilt | SURFACE_TILT_DEFAULT |
surface_tilt_threshold | surface_tilt_threshold | SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD | |
albedo | float | None | albedo | ALBEDO_DEFAULT |
global_horizontal_irradiance | ndarray | None | global_horizontal_irradiance | None |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | LOG_LEVEL_DEFAULT |
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/ground_reflected.py
@log_function_call
@custom_cached
def calculate_ground_reflected_inclined_irradiance_series_pvgis(
longitude: float,
latitude: float,
elevation: float,
timestamps: DatetimeIndex = DatetimeIndex([Timestamp.now(tz='UTC')]),
surface_orientation: float = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: float = SURFACE_TILT_DEFAULT,
surface_tilt_threshold = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
albedo: float | None = ALBEDO_DEFAULT,
global_horizontal_irradiance: ndarray | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
):
"""Calculate the clear-sky diffuse ground reflected irradiance on an inclined surface (Ri).
The calculation assumes isotropy. The ground reflected clear-sky irradiance
received on an inclined surface [W.m-2] is proportional to the global
horizontal irradiance Ghc, to the mean ground albedo ρg and a fraction of
the ground viewed by an inclined surface rg(γN).
Parameters
----------
longitude : float
longitude
latitude : float
latitude
elevation : float
elevation
timestamps : DatetimeIndex
timestamps
surface_orientation : float
surface_orientation
surface_tilt : float
surface_tilt
surface_tilt_threshold :
surface_tilt_threshold
albedo : float | None
albedo
global_horizontal_irradiance : ndarray | None
global_horizontal_irradiance
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
"""
if surface_tilt <= surface_tilt_threshold: # No ground reflection for a flat or nearly flat surface
ground_view_fraction = 0
# in order to avoid 'NameError's
flat_surface_array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from timestamps
global_horizontal_irradiance = create_array(**flat_surface_array_parameters)
else:
ground_view_fraction = (1 - cos(surface_tilt)) / 2
ground_reflected_inclined_irradiance_series = asarray(
global_horizontal_irradiance * ground_view_fraction * albedo,
dtype=dtype
).reshape(-1)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=ground_reflected_inclined_irradiance_series,
shape=timestamps.shape,
data_model=DiffuseGroundReflectedInclinedIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=ground_reflected_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
# Common output structure
function_output = {
"value": ground_reflected_inclined_irradiance_series,
"out_of_range": out_of_range,
"out_of_range_index": out_of_range_index,
"ground_view_fraction": ground_view_fraction,
"albedo": albedo,
"global_horizontal_irradiance": global_horizontal_irradiance,
"location": (longitude, latitude),
"elevation": elevation,
"surface_orientation": surface_orientation,
"surface_tilt": surface_tilt,
"surface_tilt_threshold": surface_tilt_threshold,
}
return DiffuseGroundReflectedInclinedIrradiance(**function_output)
horizontal ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_horizontal_irradiance_hofierka | Calculate the diffuse horizontal irradiance from SARAH time series. |
calculate_diffuse_horizontal_irradiance_hofierka ¶
calculate_diffuse_horizontal_irradiance_hofierka(
global_horizontal_irradiance_series: ndarray,
direct_horizontal_irradiance_series: ndarray,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> (
DiffuseSkyReflectedHorizontalIrradianceFromExternalData
)
Calculate the diffuse horizontal irradiance from SARAH time series.
Calculate the diffuse horizontal irradiance incident on a solar surface from SARAH time series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
global_horizontal_irradiance_series | ndarray | The global horizontal irradiance, also known as surface short-wave solar radiation downwards, is the solar radiation that reaches a horizontal plane at the surface of the Earth. This parameter comprises both direct and diffuse solar radiation. | required |
direct_horizontal_irradiance_series | ndarray | direct_horizontal_irradiance_series | required |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
DiffuseSkyReflectedHorizontalIrradianceFromExternalData | The diffuse radiant flux incident on a surface per unit area in W/m². |
Notes
The corresponding ECMWF product variable is named `ssrd`.
Source code in pvgisprototype/algorithms/hofierka/irradiance/diffuse/horizontal.py
@log_function_call
@custom_cached
def calculate_diffuse_horizontal_irradiance_hofierka(
global_horizontal_irradiance_series: ndarray,
direct_horizontal_irradiance_series: ndarray,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT, # Not yet integrated !
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedHorizontalIrradianceFromExternalData:
"""Calculate the diffuse horizontal irradiance from SARAH time series.
Calculate the diffuse horizontal irradiance incident on a solar surface
from SARAH time series.
Parameters
----------
global_horizontal_irradiance_series : ndarray
The global horizontal irradiance, also known as surface short-wave
solar radiation downwards, is the solar radiation that reaches a
horizontal plane at the surface of the Earth. This parameter comprises
both direct and diffuse solar radiation.
direct_horizontal_irradiance_series : ndarray
direct_horizontal_irradiance_series
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
Returns
-------
DiffuseSkyReflectedHorizontalIrradianceFromExternalData
The diffuse radiant flux incident on a surface per unit area in W/m².
Notes
-----
The corresponding ECMWF product variable is named `ssrd`.
"""
diffuse_horizontal_irradiance_series = (
global_horizontal_irradiance_series - direct_horizontal_irradiance_series
).astype(dtype=dtype)
if diffuse_horizontal_irradiance_series.size == 1 and diffuse_horizontal_irradiance_series.shape == ():
diffuse_horizontal_irradiance_series = numpy.array(
[diffuse_horizontal_irradiance_series], dtype=dtype
)
single_value = float(diffuse_horizontal_irradiance_series)
warning = (
f"{exclamation_mark} The selected timestamp "
+ " matches the single value "
+ f"{single_value}"
)
logger.warning(warning)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=diffuse_horizontal_irradiance_series,
shape=global_horizontal_irradiance_series.shape,
data_model=DiffuseSkyReflectedHorizontalIrradianceFromExternalData(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=diffuse_horizontal_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DiffuseSkyReflectedHorizontalIrradianceFromExternalData(
value=diffuse_horizontal_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
global_horizontal_irradiance=global_horizontal_irradiance_series,
direct_horizontal_irradiance=direct_horizontal_irradiance_series,
)
direct ¶
Modules:
| Name | Description |
|---|---|
clear_sky | |
inclined | This Python module is part of PVGIS' API. It implements functions to calculate |
normal | This Python module is part of PVGIS' Algorithms. It implements a function to |
clear_sky ¶
Modules:
| Name | Description |
|---|---|
horizontal | This Python module is part of PVGIS' Algorithms. It implements functions to calculate |
inclined | This Python module is part of PVGIS' API. It implements functions to calculate |
linke_turbidity_factor | |
normal | This Python module is part of PVGIS' Algorithms. It implements a function to |
horizontal ¶
This Python module is part of PVGIS' Algorithms. It implements functions to calculate the direct solar irradiance.
Direct or beam irradiance is one of the main components of solar irradiance. It comes perpendicular from the Sun and is not scattered before it irradiates a surface.
During a cloudy day the sunlight will be partially absorbed and scattered by different air molecules. The latter part is defined as the diffuse irradiance. The remaining part is the direct irradiance.
Functions:
| Name | Description |
|---|---|
calculate_clear_sky_direct_horizontal_irradiance_hofierka | Calculate the direct horizontal irradiance |
calculate_clear_sky_direct_horizontal_irradiance_hofierka ¶calculate_clear_sky_direct_horizontal_irradiance_hofierka(
elevation: float,
timestamps: DatetimeIndex = DatetimeIndex(
[now(tz="UTC")]
),
solar_altitude_series: SolarAltitude | None = None,
surface_in_shade_series: LocationShading | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectHorizontalIrradiance
Calculate the direct horizontal irradiance
This function implements the algorithm described by Hofierka [1]_.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
elevation | float | elevation | required |
timestamps | DatetimeIndex | timestamps | DatetimeIndex([now(tz='UTC')]) |
solar_altitude_series | SolarAltitude | None | solar_altitude_series | None |
surface_in_shade_series | LocationShading | None | surface_in_shade_series | None |
linke_turbidity_factor_series | LinkeTurbidityFactor | linke_turbidity_factor_series | LinkeTurbidityFactor() |
solar_constant | float | solar_constant | SOLAR_CONSTANT |
eccentricity_phase_offset | float | eccentricity_phase_offset | ECCENTRICITY_PHASE_OFFSET |
eccentricity_amplitude | float | eccentricity_amplitude | ECCENTRICITY_CORRECTION_FACTOR |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
DirectHorizontalIrradiance | |
Notes
Known also as : SID, units : W*m-2
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/direct/clear_sky/horizontal.py
@log_function_call
@custom_cached
def calculate_clear_sky_direct_horizontal_irradiance_hofierka(
elevation: float,
timestamps: DatetimeIndex = DatetimeIndex([Timestamp.now(tz='UTC')]),
solar_altitude_series: SolarAltitude | None = None,
surface_in_shade_series: LocationShading | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectHorizontalIrradiance:
"""Calculate the direct horizontal irradiance
This function implements the algorithm described by Hofierka [1]_.
Parameters
----------
elevation : float
elevation
timestamps : DatetimeIndex
timestamps
solar_altitude_series : SolarAltitude | None
solar_altitude_series
surface_in_shade_series : LocationShading | None
surface_in_shade_series
linke_turbidity_factor_series : LinkeTurbidityFactor
linke_turbidity_factor_series
solar_constant : float
solar_constant
eccentricity_phase_offset : float
eccentricity_phase_offset
eccentricity_amplitude : float
eccentricity_amplitude
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
Returns
-------
DirectHorizontalIrradiance
Notes
-----
Known also as : SID, units : W*m-2
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
# expects solar altitude in degrees! ----------------------------------vvv
#
refracted_solar_altitude_series = calculate_refracted_solar_altitude_series(
solar_altitude_series=solar_altitude_series, # expects altitude in degrees!
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
optical_air_mass_series = calculate_optical_air_mass_series(
elevation=elevation,
refracted_solar_altitude_series=refracted_solar_altitude_series,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
#
# ^^^ ---------------------------------------- expects solar altitude in degrees!
direct_normal_irradiance_series = calculate_direct_normal_irradiance_hofierka(
timestamps=timestamps,
linke_turbidity_factor_series=linke_turbidity_factor_series,
optical_air_mass_series=optical_air_mass_series,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
)
# Mask conditions
mask_solar_altitude_positive = solar_altitude_series.radians > 0
mask_not_in_shade = ~surface_in_shade_series.value
mask_sunlit_surface_series = np.logical_and.reduce((mask_solar_altitude_positive, mask_not_in_shade))
# Initialize the direct irradiance series to zeros
array_parameters = {
"shape": timestamps.shape, # Borrow shape from timestamps
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
}
direct_horizontal_irradiance_series = create_array(**array_parameters)
if np.any(mask_sunlit_surface_series):
direct_horizontal_irradiance_series[mask_sunlit_surface_series] = (
direct_normal_irradiance_series.value
* np.sin(solar_altitude_series.radians)
)[mask_sunlit_surface_series]
out_of_range, out_of_range_index = identify_values_out_of_range(
series=direct_horizontal_irradiance_series,
shape=timestamps.shape,
data_model=DirectHorizontalIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=direct_horizontal_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DirectHorizontalIrradiance(
value=direct_horizontal_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
elevation=elevation,
solar_altitude=solar_altitude_series,
refracted_solar_altitude=refracted_solar_altitude_series.value,
optical_air_mass=optical_air_mass_series,
direct_normal_irradiance=direct_normal_irradiance_series,
surface_in_shade=surface_in_shade_series,
solar_radiation_model=HOFIERKA_2002,
data_source=HOFIERKA_2002,
)
inclined ¶
This Python module is part of PVGIS' API. It implements functions to calculate the direct solar irradiance.
Direct or beam irradiance is one of the main components of solar irradiance. It comes perpendicular from the Sun and is not scattered before it irradiates a surface.
During a cloudy day the sunlight will be partially absorbed and scattered by different air molecules. The latter part is defined as the diffuse irradiance. The remaining part is the direct irradiance.
Functions:
| Name | Description |
|---|---|
calculate_clear_sky_direct_inclined_irradiance_hofierka | Calculate the direct irradiance incident on a tilted surface [W*m-2]. |
calculate_clear_sky_direct_inclined_irradiance_hofierka ¶calculate_clear_sky_direct_inclined_irradiance_hofierka(
elevation: float,
surface_orientation: (
SurfaceOrientation | None
) = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt | None = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex | None = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo | None = ZoneInfo("UTC"),
direct_horizontal_irradiance: ndarray | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
apply_reflectivity_factor: bool = True,
solar_incidence_series: SolarIncidence | None = None,
solar_altitude_series: SolarAltitude | None = None,
surface_in_shade_series: LocationShading | None = None,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectInclinedIrradiance
Calculate the direct irradiance incident on a tilted surface [W*m-2].
Calculate the direct irradiance on an inclined surface based on the solar radiation model by Hofierka, 2002. [1]_
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
elevation | float | elevation | required |
surface_orientation | SurfaceOrientation | None | surface_orientation | SURFACE_ORIENTATION_DEFAULT |
surface_tilt | SurfaceTilt | None | surface_tilt | SURFACE_TILT_DEFAULT |
timestamps | DatetimeIndex | None | timestamps | DatetimeIndex([now(tz='UTC')]) |
timezone | ZoneInfo | None | timezone | ZoneInfo('UTC') |
direct_horizontal_irradiance | ndarray | None | direct_horizontal_irradiance | None |
linke_turbidity_factor_series | LinkeTurbidityFactor | linke_turbidity_factor_series | LinkeTurbidityFactor() |
apply_reflectivity_factor | bool | apply_reflectivity_factor | True |
solar_incidence_series | SolarIncidence | None | solar_incidence_series | None |
solar_altitude_series | SolarAltitude | None | solar_altitude_series | None |
surface_in_shade_series | LocationShading | None | surface_in_shade_series | None |
solar_constant | float | solar_constant | SOLAR_CONSTANT |
eccentricity_phase_offset | float | eccentricity_phase_offset | ECCENTRICITY_PHASE_OFFSET |
eccentricity_amplitude | float | eccentricity_amplitude | ECCENTRICITY_CORRECTION_FACTOR |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
validate_output | bool | validate_output | VALIDATE_OUTPUT_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
DirectInclinedIrradiance | |
Notes
In Hofierka (2002) [1]_, see equation 11 :
Bic = B0c sin δexp
or equation 12 :
B ⋅ sin ⎛δ ⎞
hc ⎝ exp⎠ ⎛ W ⎞
B = ──────────────── in ⎜───⎟
ic sin ⎛h ⎞ ⎜ -2⎟
⎝ 0⎠ ⎝m ⎠
where :
- δexp is the solar incidence angle measured between the sun and an inclined surface defined in equation 16
or else :
Direct Inclined = Direct Horizontal * sin( Solar Incidence ) / sin( Solar Altitude )
The implementation by Hofierka (2002) uses the solar incidence angle between the sun-vector and the plane of the reference surface (as per Jenčo, 1992). This is very important and relates to the hardcoded value True for the complementary_incidence_angle input parameter of the function. We call this angle (definition) the complementary incidence angle.
For the losses due to reflectivity, the incidence angle modifier by Martin & Ruiz (2005) expects the incidence angle between the sun-vector and the surface-normal. Hence, the respective call of the function calculate_reflectivity_factor_for_direct_irradiance_series(), expects the complement of the angle defined by Jenčo (1992). We call the incidence angle expected by the incidence angle modifier by Martin & Ruiz (2005) the typical incidence angle.
See also the documentation of the function calculate_solar_incidence_series_jenco().
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/direct/clear_sky/inclined.py
@log_function_call
@custom_cached
def calculate_clear_sky_direct_inclined_irradiance_hofierka(
elevation: float,
surface_orientation: SurfaceOrientation | None = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt | None = SURFACE_TILT_DEFAULT,
#
timestamps: DatetimeIndex | None = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo | None = ZoneInfo('UTC'),
#
direct_horizontal_irradiance: ndarray | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
apply_reflectivity_factor: bool = True,
solar_incidence_series: SolarIncidence | None = None,
solar_altitude_series: SolarAltitude | None = None,
# solar_azimuth_series: SolarAzimuth | None = None,
surface_in_shade_series: LocationShading | None = None,
#
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
#
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
#
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectInclinedIrradiance:
"""Calculate the direct irradiance incident on a tilted surface [W*m-2].
Calculate the direct irradiance on an inclined surface based on the
solar radiation model by Hofierka, 2002. [1]_
Parameters
----------
elevation : float
elevation
surface_orientation : SurfaceOrientation | None
surface_orientation
surface_tilt : SurfaceTilt | None
surface_tilt
timestamps : DatetimeIndex | None
timestamps
timezone : ZoneInfo | None
timezone
direct_horizontal_irradiance : ndarray | None
direct_horizontal_irradiance
linke_turbidity_factor_series : LinkeTurbidityFactor
linke_turbidity_factor_series
apply_reflectivity_factor : bool
apply_reflectivity_factor
solar_incidence_series : SolarIncidence | None
solar_incidence_series
solar_altitude_series : SolarAltitude | None
solar_altitude_series
surface_in_shade_series : LocationShading | None
surface_in_shade_series
solar_constant : float
solar_constant
eccentricity_phase_offset : float
eccentricity_phase_offset
eccentricity_amplitude : float
eccentricity_amplitude
dtype : str
dtype
array_backend : str
array_backend
validate_output : bool
validate_output
verbose : int
verbose
log : int
log
Returns
-------
DirectInclinedIrradiance
Notes
-----
In Hofierka (2002) [1]_, see equation 11 :
Bic = B0c sin δexp
or equation 12 :
B ⋅ sin ⎛δ ⎞
hc ⎝ exp⎠ ⎛ W ⎞
B = ──────────────── in ⎜───⎟
ic sin ⎛h ⎞ ⎜ -2⎟
⎝ 0⎠ ⎝m ⎠
where :
- δexp is the solar incidence angle measured between the sun and an
inclined surface defined in equation 16
or else :
Direct Inclined = Direct Horizontal * sin( Solar Incidence ) / sin( Solar Altitude )
The implementation by Hofierka (2002) uses the solar incidence angle
between the sun-vector and the plane of the reference surface (as per Jenčo,
1992). This is very important and relates to the hardcoded value `True` for
the `complementary_incidence_angle` input parameter of the function. We
call this angle (definition) the _complementary_ incidence angle.
For the losses due to reflectivity, the incidence angle modifier by Martin
& Ruiz (2005) expects the incidence angle between the sun-vector and the
surface-normal. Hence, the respective call of the function
`calculate_reflectivity_factor_for_direct_irradiance_series()`,
expects the complement of the angle defined by Jenčo (1992). We call the
incidence angle expected by the incidence angle modifier by Martin & Ruiz
(2005) the _typical_ incidence angle.
See also the documentation of the function
`calculate_solar_incidence_series_jenco()`.
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
# Create a sunlit surface time series mask
# - solar altitude > 0
# - surface not in shade
# - solar incidence > 0
mask_solar_altitude_positive = solar_altitude_series.radians > 0
# Following, the _complementary_ solar incidence angle is used (Jenčo, 1992)!
mask_solar_incidence_positive = solar_incidence_series.radians > 0
mask_not_in_shade = ~surface_in_shade_series.value
mask_sunlit_surface_series = numpy.logical_and.reduce(
(mask_solar_altitude_positive, mask_solar_incidence_positive, mask_not_in_shade)
)
# Else, the following runs:
# --------------------------------- Review & Add ?
# 1. surface is shaded
# 3. solar incidence = 0
# --------------------------------- Review & Add ?
# ========================================================================
if direct_horizontal_irradiance is not None:
logger.error(
"The `direct_horizontal_irradiance` should be None at this point !",
alt = ":information: [bold red]The [code]direct_horizontal_irradiance[/code] input should be [code]None[/code] at this point ![/bold red]",
)
raise ValueError(
":information: The `direct_horizontal_irradiance` input should be None at this point !",
)
else:
if verbose > 0:
logger.debug(
":information: Modelling direct horizontal irradiance...",
alt=":information: [bold][magenta]Modelling[/magenta] direct horizontal irradiance[/bold]...",
)
direct_horizontal_irradiance_series = (
calculate_clear_sky_direct_horizontal_irradiance_hofierka(
elevation=elevation,
timestamps=timestamps,
solar_altitude_series=solar_altitude_series,
surface_in_shade_series=surface_in_shade_series,
linke_turbidity_factor_series=linke_turbidity_factor_series,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
)
try:
# the number of timestamps should match the number of "x" values
if verbose > 0:
logger.debug(
"\ni Calculating the direct inclined irradiance ..",
alt="\ni [bold]Calculating[/bold] the [magenta]direct inclined irradiance[/magenta] .."
)
compare_temporal_resolution(
timestamps, direct_horizontal_irradiance_series.value
)
# Else, the following runs:
# --------------------------------- Review & Add ?
# 1. surface is shaded
# 3. solar incidence = 0
# --------------------------------- Review & Add ?
# ========================================================================
# Initialize the direct irradiance series to zeros
# array_parameters = {
# "shape": timestamps.shape,
# "dtype": dtype,
# "init_method": "zeros",
# "backend": array_backend,
# } # Borrow shape from timestamps
# direct_inclined_irradiance_series = create_array(**array_parameters)
# if mask_sunlit_surface_series.any():
direct_inclined_irradiance_series = (
direct_horizontal_irradiance_series.value
* sin(
solar_incidence_series.radians
) # Should be the _complementary_ incidence angle!
/ sin(solar_altitude_series.radians)
)
except ZeroDivisionError:
logger.error(
"Error: Division by zero in calculating the direct inclined irradiance!"
)
logger.debug(
"Is the solar altitude angle zero ?",
alt="[bold red]Is the solar altitude angle zero ?"
)
# should this return something? Like in r.sun's simpler's approach?
raise ValueError
if not apply_reflectivity_factor:
direct_inclined_irradiance_before_reflectivity_series = None
direct_inclined_irradiance_reflectivity_factor_series = None
else:
# Calculate the reflectivity factor
#
# per Martin & Ruiz 2005 :
# expects the _typical_ sun-vector-to-normal-of-surface incidence angles
# which is the _complement_ of the incidence angle per Hofierka 2002
direct_inclined_irradiance_reflectivity_factor_series = (
calculate_reflectivity_factor_for_direct_irradiance_series(
solar_incidence_series=(pi / 2 - solar_incidence_series.radians),
verbose=0,
)
)
# Apply the reflectivity factor
direct_inclined_irradiance_series *= (
direct_inclined_irradiance_reflectivity_factor_series
)
# Avoid copying to save memory and time ... ? ----------------- Is this safe ? -
with errstate(divide="ignore", invalid="ignore"):
# this quantity is exclusively generated for the output dictionary !
direct_inclined_irradiance_before_reflectivity_series = where(
direct_inclined_irradiance_reflectivity_factor_series != 0,
direct_inclined_irradiance_series
/ direct_inclined_irradiance_reflectivity_factor_series,
0,
)
# ------------------------------------------------------------------------------
out_of_range, out_of_range_index = identify_values_out_of_range(
series=direct_inclined_irradiance_series,
shape=timestamps.shape,
data_model=DirectInclinedIrradiance(),
)
if numpy.any(direct_inclined_irradiance_series < 0):
logger.warning(
"\n[red]Warning: Negative values found in `direct_inclined_irradiance_series`![/red]"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=direct_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DirectInclinedIrradiance(
value=direct_inclined_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
## Irradiance components
direct_horizontal_irradiance=direct_horizontal_irradiance_series,
reflected = calculate_reflectivity_effect(
irradiance=direct_inclined_irradiance_before_reflectivity_series,
reflectivity_factor=direct_inclined_irradiance_reflectivity_factor_series,
),
reflectivity_factor=direct_inclined_irradiance_reflectivity_factor_series,
value_before_reflectivity=direct_inclined_irradiance_before_reflectivity_series,
## Location
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
#
solar_positioning_algorithm=solar_incidence_series.solar_positioning_algorithm,
solar_timing_algorithm=solar_incidence_series.solar_timing_algorithm,
refracted_solar_altitude=direct_horizontal_irradiance_series.refracted_solar_altitude,
solar_incidence_model=solar_incidence_series.algorithm,
solar_incidence_definition=solar_incidence_series.definition,
shading_algorithm=surface_in_shade_series.shading_algorithm,
#
surface_in_shade=surface_in_shade_series,
solar_incidence=solar_incidence_series,
solar_altitude=solar_altitude_series,
# solar_azimuth=solar_azimuth_series,
#
linke_turbidity_factor=linke_turbidity_factor_series,
)
linke_turbidity_factor ¶
Functions:
| Name | Description |
|---|---|
correct_linke_turbidity_factor_series | Calculate the air mass 2 Linke turbidity factor. |
correct_linke_turbidity_factor_series ¶correct_linke_turbidity_factor_series(
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
) -> LinkeTurbidityFactor
Calculate the air mass 2 Linke turbidity factor.
Calculate the air mass 2 Linke atmospheric turbidity factor for a time series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linke_turbidity_factor_series | LinkeTurbidityFactor | The Linke turbidity factors as a list of LinkeTurbidityFactor objects or a single object. | LinkeTurbidityFactor() |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | 0 |
Returns:
| Type | Description |
|---|---|
List[LinkeTurbidityFactor] or LinkeTurbidityFactor | The corrected Linke turbidity factors as a list of LinkeTurbidityFactor objects or a single object. |
Source code in pvgisprototype/algorithms/hofierka/irradiance/direct/clear_sky/linke_turbidity_factor.py
@log_function_call
@custom_cached
def correct_linke_turbidity_factor_series(
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
) -> LinkeTurbidityFactor:
"""Calculate the air mass 2 Linke turbidity factor.
Calculate the air mass 2 Linke atmospheric turbidity factor for a time series.
Parameters
----------
linke_turbidity_factor_series: List[LinkeTurbidityFactor] | LinkeTurbidityFactor
The Linke turbidity factors as a list of LinkeTurbidityFactor objects
or a single object.
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
Returns
-------
List[LinkeTurbidityFactor] or LinkeTurbidityFactor
The corrected Linke turbidity factors as a list of LinkeTurbidityFactor
objects or a single object.
"""
corrected_linke_turbidity_factors = -0.8662 * numpy_array(
linke_turbidity_factor_series.value, dtype=dtype
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=corrected_linke_turbidity_factors,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return LinkeTurbidityFactor(
value=corrected_linke_turbidity_factors,
)
normal ¶
This Python module is part of PVGIS' Algorithms. It implements a function to calculate the direct normal solar irradiance.
Direct or beam irradiance is one of the main components of solar irradiance. It comes perpendicular from the Sun and is not scattered before it irradiates a surface.
During a cloudy day the sunlight will be partially absorbed and scattered by different air molecules. The latter part is defined as the diffuse irradiance. The remaining part is the direct irradiance.
Functions:
| Name | Description |
|---|---|
calculate_direct_normal_irradiance_hofierka | Calculate the direct normal irradiance. |
calculate_direct_normal_irradiance_hofierka ¶calculate_direct_normal_irradiance_hofierka(
timestamps: DatetimeIndex | None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
optical_air_mass_series: OpticalAirMass = [
OPTICAL_AIR_MASS_TIME_SERIES_DEFAULT
],
clip_to_physically_possible_limits: bool = True,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectNormalIrradiance
Calculate the direct normal irradiance.
The direct normal irradiance attenuated by the cloudless atmosphere, represents the amount of solar radiation received per unit area by a surface that is perpendicular (normal) to the rays that come in a straight line from the direction of the sun at its current position in the sky.
This function implements the algorithm described by Hofierka, 2002. [1]_
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamps | DatetimeIndex | None | timestamps | required |
linke_turbidity_factor_series | LinkeTurbidityFactor | linke_turbidity_factor_series | LinkeTurbidityFactor() |
optical_air_mass_series | OpticalAirMass | optical_air_mass_series | [OPTICAL_AIR_MASS_TIME_SERIES_DEFAULT] |
clip_to_physically_possible_limits | bool | clip_to_physically_possible_limits | True |
solar_constant | float | solar_constant | SOLAR_CONSTANT |
eccentricity_phase_offset | float | eccentricity_phase_offset | ECCENTRICITY_PHASE_OFFSET |
eccentricity_amplitude | float | eccentricity_amplitude | ECCENTRICITY_CORRECTION_FACTOR |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | LOG_LEVEL_DEFAULT |
fingerprint | bool | fingerprint | required |
Returns:
| Type | Description |
|---|---|
DirectNormalIrradiance | |
Notes
B0c = G0 exp {-0.8662 TLK m δR(m)}
where : - -0.8662 TLK is the air mass 2 Linke atmospheric turbidity factor [dimensionless] corrected by Kasten [24].
- m is the relative optical air mass [-] calculated using the formula:
m = (p/p0)/(sin h0ref + 0.50572 (h0ref + 6.07995)-1.6364)
where :
- h0ref is the corrected solar altitude h0 in degrees by the atmospheric refraction component ∆h0ref
where :
- ∆h0ref = 0.061359 (0.1594+1.123 h0 + 0.065656 h02)/(1 + 28.9344 h0 + 277.3971 h02)
-
h0ref = h0 + ∆h0ref
-
The p/p0 component is correction for given elevation z [m]:
p/p0 = exp (-z/8434.5)
-
δR(m) is the Rayleigh optical thickness at air mass m and is calculated according to the improved formula by Kasten as follows:
-
for m <= 20:
δR(m) = 1/(6.6296 + 1.7513 m - 0.1202 m2 + 0.0065 m3 - 0.00013 m4)
- for m > 20
δR(m) = 1/(10.4 + 0.718 m)
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/direct/clear_sky/normal.py
@log_function_call
@custom_cached
def calculate_direct_normal_irradiance_hofierka(
timestamps: DatetimeIndex | None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
optical_air_mass_series: OpticalAirMass = [
OPTICAL_AIR_MASS_TIME_SERIES_DEFAULT
], # REVIEW-ME + ?
clip_to_physically_possible_limits: bool = True,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectNormalIrradiance:
"""Calculate the direct normal irradiance.
The direct normal irradiance attenuated by the cloudless atmosphere,
represents the amount of solar radiation received per unit area by a
surface that is perpendicular (normal) to the rays that come in a straight
line from the direction of the sun at its current position in the sky.
This function implements the algorithm described by Hofierka, 2002. [1]_
Parameters
----------
timestamps : DatetimeIndex | None
timestamps
linke_turbidity_factor_series : LinkeTurbidityFactor
linke_turbidity_factor_series
optical_air_mass_series : OpticalAirMass
optical_air_mass_series
clip_to_physically_possible_limits : bool
clip_to_physically_possible_limits
solar_constant : float
solar_constant
eccentricity_phase_offset : float
eccentricity_phase_offset
eccentricity_amplitude : float
eccentricity_amplitude
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
fingerprint : bool
fingerprint
Returns
-------
DirectNormalIrradiance
Notes
-----
B0c = G0 exp {-0.8662 TLK m δR(m)}
where :
- -0.8662 TLK is the air mass 2 Linke atmospheric turbidity factor [dimensionless] corrected by Kasten [24].
- m is the relative optical air mass [-] calculated using the formula:
m = (p/p0)/(sin h0ref + 0.50572 (h0ref + 6.07995)-1.6364)
where :
- h0ref is the corrected solar altitude h0 in degrees by the atmospheric refraction component ∆h0ref
where :
- ∆h0ref = 0.061359 (0.1594+1.123 h0 + 0.065656 h02)/(1 + 28.9344 h0 + 277.3971 h02)
- h0ref = h0 + ∆h0ref
- The p/p0 component is correction for given elevation z [m]:
p/p0 = exp (-z/8434.5)
- δR(m) is the Rayleigh optical thickness at air mass m and is calculated according to the improved formula by Kasten as follows:
- for m <= 20:
δR(m) = 1/(6.6296 + 1.7513 m - 0.1202 m2 + 0.0065 m3 - 0.00013 m4)
- for m > 20
δR(m) = 1/(10.4 + 0.718 m)
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
extraterrestrial_normal_irradiance_series = calculate_extraterrestrial_normal_irradiance_hofierka(
timestamps=timestamps,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
)
corrected_linke_turbidity_factor_series = correct_linke_turbidity_factor_series(
linke_turbidity_factor_series,
verbose=verbose,
)
rayleigh_optical_thickness_series = calculate_rayleigh_optical_thickness_series(
optical_air_mass_series,
verbose=verbose,
) # _quite_ high when the sun is below the horizon. Makes sense ?
# Calculate
with np.errstate(divide="ignore", invalid="ignore", over="ignore"):
exponent = (
corrected_linke_turbidity_factor_series.value
* optical_air_mass_series.value
* rayleigh_optical_thickness_series.value
)
direct_normal_irradiance_series = extraterrestrial_normal_irradiance_series.value * np.exp(exponent)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=direct_normal_irradiance_series,
shape=timestamps.shape,
data_model=DirectNormalIrradiance(),
clip_to_physically_possible_limits=clip_to_physically_possible_limits,
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=direct_normal_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DirectNormalIrradiance(
value=direct_normal_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
#
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
linke_turbidity_factor_adjusted=corrected_linke_turbidity_factor_series,
linke_turbidity_factor=linke_turbidity_factor_series,
rayleigh_optical_thickness=rayleigh_optical_thickness_series,
optical_air_mass=optical_air_mass_series,
#
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
)
inclined ¶
This Python module is part of PVGIS' API. It implements functions to calculate the direct solar irradiance.
Direct or beam irradiance is one of the main components of solar irradiance. It comes perpendicular from the Sun and is not scattered before it irradiates a surface.
During a cloudy day the sunlight will be partially absorbed and scattered by different air molecules. The latter part is defined as the diffuse irradiance. The remaining part is the direct irradiance.
Functions:
| Name | Description |
|---|---|
calculate_direct_inclined_irradiance_hofierka | Calculate the direct irradiance incident on a tilted surface [W*m-2]. |
calculate_direct_inclined_irradiance_hofierka ¶
calculate_direct_inclined_irradiance_hofierka(
timestamps: DatetimeIndex | None = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo | None = ZoneInfo("UTC"),
direct_horizontal_irradiance: ndarray | None = None,
apply_reflectivity_factor: bool = True,
solar_incidence_series: SolarIncidence | None = None,
solar_altitude_series: SolarAltitude | None = None,
surface_in_shade_series: LocationShading | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectInclinedIrradianceFromExternalData
Calculate the direct irradiance incident on a tilted surface [W*m-2].
Calculate the direct irradiance on an inclined surface based on the solar radiation model by Hofierka, 2002. [1]_
Notes
Bic = B0c sin δexp (equation 11)
or
B ⋅ sin ⎛δ ⎞
hc ⎝ exp⎠ ⎛ W ⎞
B = ──────────────── in ⎜───⎟ ic sin ⎛h ⎞ ⎜ -2⎟ ⎝ 0⎠ ⎝m ⎠
(equation 12)
where :
- δexp is the solar incidence angle measured between the sun and an inclined surface defined in equation (16).
or else :
Direct Inclined = Direct Horizontal * sin( Solar Incidence ) / sin( Solar Altitude )
The implementation by Hofierka (2002) uses the solar incidence angle between the sun-vector and the plane of the reference surface (as per Jenčo, 1992). This is very important and relates to the hardcoded value True for the complementary_incidence_angle input parameter of the function. We call this angle (definition) the complementary incidence angle.
For the losses due to reflectivity, the incidence angle modifier by Martin & Ruiz (2005) expects the incidence angle between the sun-vector and the surface-normal. Hence, the respective call of the function calculate_reflectivity_factor_for_direct_irradiance_series(), expects the complement of the angle defined by Jenčo (1992). We call the incidence angle expected by the incidence angle modifier by Martin & Ruiz (2005) the typical incidence angle.
See also the documentation of the function calculate_solar_incidence_series_jenco().
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/direct/inclined.py
@log_function_call
@custom_cached
def calculate_direct_inclined_irradiance_hofierka(
timestamps: DatetimeIndex | None = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo | None = ZoneInfo('UTC'),
direct_horizontal_irradiance: ndarray | None = None,
apply_reflectivity_factor: bool = True,
solar_incidence_series: SolarIncidence | None = None,
solar_altitude_series: SolarAltitude | None = None,
# solar_azimuth_series: SolarAzimuth | None = None,
surface_in_shade_series: LocationShading | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DirectInclinedIrradianceFromExternalData:
"""Calculate the direct irradiance incident on a tilted surface [W*m-2].
Calculate the direct irradiance on an inclined surface based on the
solar radiation model by Hofierka, 2002. [1]_
Notes
-----
Bic = B0c sin δexp (equation 11)
or
B ⋅ sin ⎛δ ⎞
hc ⎝ exp⎠ ⎛ W ⎞
B = ──────────────── in ⎜───⎟
ic sin ⎛h ⎞ ⎜ -2⎟
⎝ 0⎠ ⎝m ⎠
(equation 12)
where :
- δexp is the solar incidence angle measured between the sun and an
inclined surface defined in equation (16).
or else :
Direct Inclined = Direct Horizontal * sin( Solar Incidence ) / sin( Solar Altitude )
The implementation by Hofierka (2002) uses the solar incidence angle
between the sun-vector and the plane of the reference surface (as per Jenčo,
1992). This is very important and relates to the hardcoded value `True` for
the `complementary_incidence_angle` input parameter of the function. We
call this angle (definition) the _complementary_ incidence angle.
For the losses due to reflectivity, the incidence angle modifier by Martin
& Ruiz (2005) expects the incidence angle between the sun-vector and the
surface-normal. Hence, the respective call of the function
`calculate_reflectivity_factor_for_direct_irradiance_series()`,
expects the complement of the angle defined by Jenčo (1992). We call the
incidence angle expected by the incidence angle modifier by Martin & Ruiz
(2005) the _typical_ incidence angle.
See also the documentation of the function
`calculate_solar_incidence_series_jenco()`.
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
# Create a Sunlit surface time series mask :
# - solar altitude > 0
# - surface not in shade
# - solar incidence > 0
mask_solar_altitude_positive = solar_altitude_series.radians > 0
# Here we use the _complementary_ solar incidence angle,
# sun-to-surface-plane as per Jenčo 1992.
mask_solar_incidence_positive = solar_incidence_series.radians > 0
mask_not_in_shade = ~surface_in_shade_series.value
mask_sunlit_surface_series = numpy.logical_and.reduce(
(mask_solar_altitude_positive, mask_solar_incidence_positive, mask_not_in_shade)
)
# Else, the following runs:
# --------------------------------- Review & Add ?
# 1. surface is shaded
# 3. solar incidence = 0
# --------------------------------- Review & Add ?
# ========================================================================
if not isinstance(direct_horizontal_irradiance, ndarray):
logger.error(
"The `direct_horizontal_irradiance` should be a NumPy array at this point !",
alt = ":information: [bold red]The [code]direct_horizontal_irradiance[/code] input should be a [code]NumPy array[/code] at this point ![/bold red]",
)
raise ValueError(
":information: The `direct_horizontal_irradiance` input should be a NumPy array at this point !",
)
# However, generate a native data model for it :
direct_horizontal_irradiance = DirectHorizontalIrradianceFromExternalData(
value=direct_horizontal_irradiance
)
try:
# the number of timestamps should match the number of "x" values
if verbose > 0:
logger.info(
"\ni [bold]Calculating[/bold] the [magenta]direct inclined irradiance[/magenta] .."
)
compare_temporal_resolution(
timestamps, direct_horizontal_irradiance.value
)
# Else, the following runs:
# --------------------------------- Review & Add ?
# 1. surface is shaded
# 3. solar incidence = 0
# --------------------------------- Review & Add ?
# ========================================================================
# Initialize the direct irradiance series to zeros
# array_parameters = {
# "shape": timestamps.shape,
# "dtype": dtype,
# "init_method": "zeros",
# "backend": array_backend,
# } # Borrow shape from timestamps
# direct_inclined_irradiance_series = create_array(**array_parameters)
# if mask_sunlit_surface_series.any():
direct_inclined_irradiance_series = (
direct_horizontal_irradiance.value
* sin(
solar_incidence_series.radians
) # Should be the _complementary_ incidence angle!
/ sin(solar_altitude_series.radians)
)
except ZeroDivisionError:
logger.error(
"Error: Division by zero in calculating the direct inclined irradiance!"
)
logger.debug("Is the solar altitude angle zero ?")
# should this return something? Like in r.sun's simpler's approach?
raise ValueError
if not apply_reflectivity_factor:
direct_inclined_irradiance_before_reflectivity_series = None
direct_inclined_irradiance_reflectivity_factor_series = None
else:
# Calculate the reflectivity factor
#
# per Martin & Ruiz 2005 :
# expects the _typical_ sun-vector-to-normal-of-surface incidence angles
# which is the _complement_ of the incidence angle per Hofierka 2002.
# Hence, we need to convert !
direct_inclined_irradiance_reflectivity_factor_series = (
calculate_reflectivity_factor_for_direct_irradiance_series(
solar_incidence_series=(pi / 2 - solar_incidence_series.radians),
verbose=0,
)
)
# Apply the reflectivity factor
direct_inclined_irradiance_series *= (
direct_inclined_irradiance_reflectivity_factor_series
)
# Avoid copying to save memory and time ... ? ----------------- Is this safe ? -
with errstate(divide="ignore", invalid="ignore"):
# this quantity is exclusively generated for the output dictionary !
direct_inclined_irradiance_before_reflectivity_series = where(
direct_inclined_irradiance_reflectivity_factor_series != 0,
direct_inclined_irradiance_series
/ direct_inclined_irradiance_reflectivity_factor_series,
0,
)
# ------------------------------------------------------------------------------
out_of_range, out_of_range_index = identify_values_out_of_range(
series=direct_inclined_irradiance_series,
shape=timestamps.shape,
data_model=DirectInclinedIrradianceFromExternalData(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=direct_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DirectInclinedIrradianceFromExternalData(
value=direct_inclined_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
## Irradiance components
direct_horizontal_irradiance=direct_horizontal_irradiance,
reflected = calculate_reflectivity_effect(
irradiance=direct_inclined_irradiance_before_reflectivity_series,
reflectivity_factor=direct_inclined_irradiance_reflectivity_factor_series,
),
reflectivity_factor=direct_inclined_irradiance_reflectivity_factor_series,
value_before_reflectivity=direct_inclined_irradiance_before_reflectivity_series,
## Location
surface_in_shade=surface_in_shade_series,
solar_incidence=solar_incidence_series,
solar_altitude=solar_altitude_series,
)
normal ¶
This Python module is part of PVGIS' Algorithms. It implements a function to calculate the direct normal solar irradiance.
Direct or beam irradiance is one of the main components of solar irradiance. It comes perpendicular from the Sun and is not scattered before it irradiates a surface.
During a cloudy day the sunlight will be partially absorbed and scattered by different air molecules. The latter part is defined as the diffuse irradiance. The remaining part is the direct irradiance.
Functions:
| Name | Description |
|---|---|
calculate_direct_normal_from_horizontal_irradiance_hofierka | Calculate the direct normal from the direct horizontal irradiance. |
calculate_direct_normal_from_horizontal_irradiance_hofierka ¶
calculate_direct_normal_from_horizontal_irradiance_hofierka(
direct_horizontal_irradiance: ndarray,
solar_altitude_series: SolarAltitude | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> DirectNormalFromHorizontalIrradiance
Calculate the direct normal from the direct horizontal irradiance.
The direct normal irradiance represents the amount of solar radiation received per unit area by a surface that is perpendicular (normal) to the rays that come in a straight line from the direction of the sun at its current position in the sky. This function calculates the direct normal irradiance based on the direct horizontal irradiance and the solar altitude.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
direct_horizontal_irradiance | ndarray | direct_horizontal_irradiance | required |
solar_altitude_series | SolarAltitude | None | solar_altitude_series | None |
dtype | str | dtype | DATA_TYPE_DEFAULT |
array_backend | str | array_backend | ARRAY_BACKEND_DEFAULT |
verbose | int | verbose | VERBOSE_LEVEL_DEFAULT |
log | int | log | 0 |
fingerprint | bool | fingerprint | FINGERPRINT_FLAG_DEFAULT |
Returns:
| Type | Description |
|---|---|
DirectNormalFromHorizontalIrradiance | |
Notes
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/direct/normal.py
@log_function_call
@custom_cached
def calculate_direct_normal_from_horizontal_irradiance_hofierka(
direct_horizontal_irradiance: ndarray,
solar_altitude_series: SolarAltitude | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> DirectNormalFromHorizontalIrradiance:
"""Calculate the direct normal from the direct horizontal irradiance.
The direct normal irradiance represents the amount of solar radiation
received per unit area by a surface that is perpendicular (normal) to the
rays that come in a straight line from the direction of the sun at its
current position in the sky. This function calculates the direct normal
irradiance based on the direct horizontal irradiance and the solar
altitude.
Parameters
----------
direct_horizontal_irradiance : ndarray
direct_horizontal_irradiance
solar_altitude_series : SolarAltitude | None
solar_altitude_series
dtype : str
dtype
array_backend : str
array_backend
verbose : int
verbose
log : int
log
fingerprint : bool
fingerprint
Returns
-------
DirectNormalFromHorizontalIrradiance
Notes
-----
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
mask_solar_altitude_positive = solar_altitude_series.radians > 0
mask_not_in_shade = np.full_like(
solar_altitude_series.radians, True
) # Stub, replace with actual condition
mask = np.logical_and.reduce((mask_solar_altitude_positive, mask_not_in_shade))
direct_normal_irradiance_series = np.zeros_like(solar_altitude_series.radians)
if np.any(mask):
direct_normal_irradiance_series[mask] = (
direct_horizontal_irradiance / np.sin(solar_altitude_series.radians)
)[mask]
out_of_range, out_of_range_index = identify_values_out_of_range(
series=direct_normal_irradiance_series,
shape=solar_altitude_series.value.shape,
data_model=DirectNormalFromHorizontalIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=direct_normal_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DirectNormalFromHorizontalIrradiance(
value=direct_normal_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
direct_horizontal_irradiance=direct_horizontal_irradiance,
solar_altitude=solar_altitude_series,
)
extraterrestrial ¶
Modules:
| Name | Description |
|---|---|
horizontal | |
normal | |
horizontal ¶
Functions:
| Name | Description |
|---|---|
calculate_extraterrestrial_horizontal_irradiance_series_hofierka | Calculate the horizontal extraterrestrial irradiance over a period of time. |
calculate_extraterrestrial_horizontal_irradiance_series_hofierka ¶
calculate_extraterrestrial_horizontal_irradiance_series_hofierka(
extraterrestrial_normal_irradiance: ExtraterrestrialNormalIrradiance = ExtraterrestrialNormalIrradiance(),
solar_altitude_series: SolarAltitude = SolarAltitude(),
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
) -> ExtraterrestrialHorizontalIrradiance
Calculate the horizontal extraterrestrial irradiance over a period of time. This function implements the algorithm described by Hofierka [1]_.
Notes
In [1]_ : G0h = G0 ⋅ sin(h0) or else : G0 horizontal = G0 normal ⋅ sin(solar altitude)
In the context of PVGIS, one question is : does it make sense to have negative extraterrestrial horizontal irradiance for moments in time when the sun is below the horizon (i.e. solar altitude < 0 radians or degrees) ?
For PVGIS the answer is no, hence the output horizontal extraterrestrial irradiance is set to 0 for this moments.
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/extraterrestrial/horizontal.py
@log_function_call
@custom_cached
def calculate_extraterrestrial_horizontal_irradiance_series_hofierka(
extraterrestrial_normal_irradiance: ExtraterrestrialNormalIrradiance = ExtraterrestrialNormalIrradiance(),
solar_altitude_series: SolarAltitude = SolarAltitude(),
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
) -> ExtraterrestrialHorizontalIrradiance:
"""
Calculate the horizontal extraterrestrial irradiance over a period of time.
This function implements the algorithm described by Hofierka [1]_.
Notes
-----
In [1]_ : G0h = G0 ⋅ sin(h0) or else : G0 horizontal = G0 normal ⋅ sin(solar altitude)
In the context of PVGIS, one question is : does it make sense to have negative
extraterrestrial horizontal irradiance for moments in time when the sun is
below the horizon (i.e. solar altitude < 0 radians or degrees) ?
For PVGIS the answer is no, hence the output horizontal extraterrestrial
irradiance is set to 0 for this moments.
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
if verbose > 0:
logger.info(
":information: Modelling the horizontal extraterrestrial irradiance",
alt=":information: [bold]Modelling[/bold] the horizontal extraterrestrial irradiance",
)
extraterrestrial_horizontal_irradiance = (
extraterrestrial_normal_irradiance.value * np.sin(solar_altitude_series.radians)
)
extraterrestrial_horizontal_irradiance[solar_altitude_series.radians < 0] = (
0 # In the context of PVGIS, does it make sense to have negative extraterrestrial horizontal irradiance
)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=extraterrestrial_horizontal_irradiance,
shape=solar_altitude_series.value.shape,
data_model=ExtraterrestrialHorizontalIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=extraterrestrial_horizontal_irradiance,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return ExtraterrestrialHorizontalIrradiance(
value=extraterrestrial_horizontal_irradiance,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
normal=extraterrestrial_normal_irradiance,
quality="Not validated!",
)
normal ¶
Functions:
| Name | Description |
|---|---|
calculate_extraterrestrial_normal_irradiance_hofierka | Calculate the normal extraterrestrial irradiance over a period of time. |
calculate_extraterrestrial_normal_irradiance_hofierka ¶
calculate_extraterrestrial_normal_irradiance_hofierka(
timestamps: DatetimeIndex | None,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
) -> ExtraterrestrialNormalIrradiance
Calculate the normal extraterrestrial irradiance over a period of time. This function implements the algorithm described by Hofierka [1]_.
Outside the atmosphere, at the mean solar distance, the direct (beam) irradiance, also known as the solar constant (I0), is 1367 W.m-2. The earth’s orbit is lightly eccentric and the sun- earth distance varies slightly across the year. Therefore a correction factor ε, to allow for the varying solar distance, is applied in calculation of the extraterrestrial irradiance G0 normal to the solar beam [W.m-2] :
G0 = I0 ⋅ ε (1)
where :
ε = 1 + 0.03344 cos (j’ - 0.048869) (2)
where the day angle j’ is in radians :
j’ = 2 π j/365.25 (3)
and j is the day number which varies from 1 on January 1st to 365 (366) on December 31st.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamps | DatetimeIndex | None | | required |
solar_constant | float | | SOLAR_CONSTANT |
eccentricity_phase_offset | float | | ECCENTRICITY_PHASE_OFFSET |
eccentricity_amplitude | float | | ECCENTRICITY_CORRECTION_FACTOR |
dtype | str | | DATA_TYPE_DEFAULT |
array_backend | str | | ARRAY_BACKEND_DEFAULT |
verbose | int | | VERBOSE_LEVEL_DEFAULT |
log | int | | 0 |
Returns:
| Type | Description |
|---|---|
ExtraterrestrialNormalIrradiance | |
Notes
In [1]_ : G0h = G0 sin(h0) or else : G0 horizontal = G0 ⋅ sin(solar altitude)
References
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
Source code in pvgisprototype/algorithms/hofierka/irradiance/extraterrestrial/normal.py
@log_function_call
@custom_cached
def calculate_extraterrestrial_normal_irradiance_hofierka(
timestamps: DatetimeIndex | None,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = 0,
) -> ExtraterrestrialNormalIrradiance:
"""
Calculate the normal extraterrestrial irradiance over a period of time.
This function implements the algorithm described by Hofierka [1]_.
Outside the atmosphere, at the mean solar distance, the direct (beam) irradiance, also known as
the solar constant (I0), is 1367 W.m-2. The earth’s orbit is lightly eccentric and the sun-
earth distance varies slightly across the year. Therefore a correction factor ε, to allow for
the varying solar distance, is applied in calculation of the
extraterrestrial irradiance `G0` normal to the solar beam [W.m-2] :
G0 = I0 ⋅ ε (1)
where :
ε = 1 + 0.03344 cos (j’ - 0.048869) (2)
where the day angle j’ is in radians :
j’ = 2 π j/365.25 (3)
and `j` is the day number which varies from 1 on January 1st to 365 (366) on
December 31st.
Parameters
----------
timestamps: DatetimeIndex
solar_constant: float
eccentricity_phase_offset: float
eccentricity_amplitude: float
dtype:
array_backend:
verbose: int
log: int
Returns
-------
ExtraterrestrialNormalIrradiance
Notes
-----
In [1]_ : G0h = G0 sin(h0) or else : G0 horizontal = G0 ⋅ sin(solar altitude)
References
----------
.. [1] Hofierka, J. (2002). Some title of the paper. Journal Name, vol(issue), pages.
"""
years_in_timestamps = timestamps.year
years, indices = unique(years_in_timestamps, return_inverse=True)
days_per_year = get_days_per_year(years).astype(dtype)
days_in_years = days_per_year[indices]
day_of_year_series = timestamps.dayofyear.to_numpy().astype(dtype)
# day angle == fractional year, hence : use model_fractional_year_series()
day_angle_series = 2 * pi * day_of_year_series / days_in_years
distance_correction_factor_series = 1 + eccentricity_amplitude * cos(
day_angle_series - eccentricity_phase_offset
)
extraterrestrial_normal_irradiance = (
solar_constant * distance_correction_factor_series
)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=extraterrestrial_normal_irradiance,
shape=timestamps.shape,
data_model=ExtraterrestrialNormalIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=extraterrestrial_normal_irradiance,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return ExtraterrestrialNormalIrradiance(
value=extraterrestrial_normal_irradiance,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
day_of_year=day_of_year_series,
day_angle=day_angle_series,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
distance_correction_factor=distance_correction_factor_series,
quality="Not validated!",
)
shortwave ¶
Modules:
| Name | Description |
|---|---|
clear_sky | |
inclined | API module to calculate the global (shortwave) irradiance over a |
clear_sky ¶
Modules:
| Name | Description |
|---|---|
inclined | API module to calculate the global (shortwave) irradiance over a |
inclined ¶
API module to calculate the global (shortwave) irradiance over a location for a period in time.
Functions:
| Name | Description |
|---|---|
calculate_clear_sky_global_inclined_irradiance_hofierka | Calculate the global irradiance on an inclined surface [W.m-2] |
calculate_clear_sky_global_inclined_irradiance_hofierka ¶calculate_clear_sky_global_inclined_irradiance_hofierka(
longitude: float,
latitude: float,
elevation: float,
surface_orientation: SurfaceOrientationModel = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTiltModel = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo | None = None,
global_horizontal_irradiance: ndarray | None = None,
direct_horizontal_irradiance: ndarray | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
albedo: float | None = ALBEDO_DEFAULT,
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = None,
solar_incidence_series: SolarIncidence | None = None,
solar_position_model: SolarPositionModel = noaa,
surface_in_shade_series: NpNDArray | None = None,
sun_horizon_position: List[
SunHorizonPositionModel
] = SUN_HORIZON_POSITION_DEFAULT,
shading_states: List[ShadingState] = [all],
solar_time_model: SolarTimeModel = noaa,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> GlobalInclinedIrradiance
Calculate the global irradiance on an inclined surface [W.m-2]
Calculate the global irradiance on an inclined surface as the sum of the direct, the diffuse and the ground-reflected radiation components. The radiation, selectively attenuated by the atmosphere, which is not reflected or scattered and reaches the surface directly is the direct radiation. The scattered radiation that reaches the ground is the diffuse radiation. In addition, a smaller part of radiation is reflected from the ground onto the inclined surface. Only small percents of reflected radiation contribute to inclined surfaces, thus it is sometimes ignored. PVGIS, however, inherits the solutions adopted in the r.sun solar radiation model in which both the diffuse and reflected radiation components are considered.
Source code in pvgisprototype/algorithms/hofierka/irradiance/shortwave/clear_sky/inclined.py
@log_function_call
@custom_cached
def calculate_clear_sky_global_inclined_irradiance_hofierka(
longitude: float,
latitude: float,
elevation: float,
surface_orientation: SurfaceOrientationModel = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTiltModel = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo | None = None,
global_horizontal_irradiance: ndarray | None = None,
direct_horizontal_irradiance: ndarray | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
# unrefracted_solar_zenith: UnrefractedSolarZenith | None = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT, # radians
albedo: float | None = ALBEDO_DEFAULT,
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = None,
solar_incidence_series: SolarIncidence | None = None,
solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
surface_in_shade_series: NpNDArray | None = None,
sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
shading_states: List[ShadingState] = [ShadingState.all],
solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output:bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> GlobalInclinedIrradiance:
"""Calculate the global irradiance on an inclined surface [W.m-2]
Calculate the global irradiance on an inclined surface as the sum of the
direct, the diffuse and the ground-reflected radiation components.
The radiation, selectively attenuated by the atmosphere, which is not
reflected or scattered and reaches the surface directly is the direct
radiation. The scattered radiation that reaches the ground is the
diffuse radiation. In addition, a smaller part of radiation is reflected
from the ground onto the inclined surface. Only small percents of reflected
radiation contribute to inclined surfaces, thus it is sometimes ignored.
PVGIS, however, inherits the solutions adopted in the r.sun solar radiation
model in which both the diffuse and reflected radiation components are
considered.
"""
# In order to avoid unbound errors we pre-define `_series` objects
array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from timestamps
zero_array = create_array(**array_parameters)
if not solar_azimuth_series:
solar_azimuth_series = SolarAzimuth(
value=zero_array,
unit='Unitless',
origin='Not Required!',
)
# # direct
# # ----------------------------------------------- Important !
# if direct_horizontal_irradiance is None:
# direct_horizontal_irradiance = zero_array
# # Important ! -----------------------------------------------
direct_inclined_irradiance_series = DirectInclinedIrradiance(
value=zero_array,
)
# direct_inclined_irradiance_series.reflectivity = zero_array
# direct_inclined_irradiance_series.value_before_reflectivity = zero_array
# direct_inclined_irradiance_series.reflectivity_factor = zero_array
# direct_inclined_irradiance_series.direct_horizontal_irradiance = direct_horizontal_irradiance
# direct_inclined_irradiance_series.solar_incidence_model = solar_incidence_model
# # direct_inclined_irradiance_series.solar_incidence.definition =
# # extraterrestrial normal to be fed indirectly to diffuse_inclined_irradiance
# extraterrestrial_normal_irradiance = ExtraterrestrialNormalIrradiance(value=zero_array)
# direct_normal_irradiance = DirectNormalIrradiance(
# value=zero_array,
# extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance,
# )
# direct_horizontal_irradiance = DirectHorizontalIrradiance(
# value=direct_horizontal_irradiance, direct_normal_irradiance=direct_normal_irradiance
# )
# diffuse sky-reflected
diffuse_inclined_irradiance_series = DiffuseSkyReflectedInclinedIrradiance(
value=zero_array,
solar_azimuth=solar_azimuth_series,
# direct_horizontal_irradiance=direct_horizontal_irradiance,
)
# diffuse_inclined_irradiance_series.reflected = zero_array
# diffuse_inclined_irradiance_series.value_before_reflectivity = zero_array
# diffuse_inclined_irradiance_series.reflectivity_factor = zero_array
# diffuse_inclined_irradiance_series.diffuse_horizontal_irradiance = DiffuseSkyReflectedHorizontalIrradiance(value=zero_array)
# diffuse ground-reflected
# note : there is no ground-reflected _horizontal_ component as such !
ground_reflected_inclined_irradiance_series = DiffuseGroundReflectedInclinedIrradiance(
value=zero_array
)
# ground_reflected_inclined_irradiance_series.reflectivity = zero_array
# ground_reflected_inclined_irradiance_series.value_before_reflectivity = zero_array
# ground_reflected_inclined_irradiance_series.reflectivity_factor = zero_array
global_inclined_irradiance_series = zero_array
# Select which solar positions related to the horizon to process
sun_horizon_positions = select_models(
SunHorizonPositionModel, sun_horizon_position
) # Using a Typer callback fails !
# and keep track of the position of the sun relative to the horizon
sun_horizon_position_series = create_array(
timestamps.shape, dtype="object", init_method="empty", backend=array_backend
)
# Following, create masks based on the solar altitude series --------
# For sun below the horizon
if SunHorizonPositionModel.below in sun_horizon_positions:
mask_below_horizon = solar_altitude_series.value < 0
sun_horizon_position_series[mask_below_horizon] = [SunHorizonPositionModel.below.value]
if numpy.any(mask_below_horizon):
logger.info(
f"Positions of the sun below horizon :\n{sun_horizon_position_series}",
alt=f"Positions of the sun [bold gray50]below horizon[/bold gray50] :\n{sun_horizon_position_series}"
)
# no incident radiance without direct sunlight !
direct_inclined_irradiance_series.value[mask_below_horizon] = 0
diffuse_inclined_irradiance_series.value[mask_below_horizon] = 0
ground_reflected_inclined_irradiance_series.value[mask_below_horizon] = 0
# For very low sun angles
if SunHorizonPositionModel.low_angle in sun_horizon_positions:
mask_low_angle = numpy.logical_and(
solar_altitude_series.value >= 0,
solar_altitude_series.value
< solar_altitude_series.low_angle_threshold_radians, # attribute in SolarAltitude data model
sun_horizon_position_series == None, # operate only on unset elements
)
sun_horizon_position_series[mask_low_angle] = [
SunHorizonPositionModel.low_angle.value
]
direct_inclined_irradiance_series.value[mask_low_angle] = (
0 # Direct radiation is negligible
)
# For sun above the horizon
if SunHorizonPositionModel.above in sun_horizon_positions:
mask_above_horizon = numpy.logical_and(
solar_altitude_series.value > 0,
sun_horizon_position_series == None, # operate only on unset elements
)
sun_horizon_position_series[mask_above_horizon] = [
SunHorizonPositionModel.above.value
]
# For sun above horizon and not in shade
mask_not_in_shade = ~surface_in_shade_series.value
mask_above_horizon_not_in_shade = numpy.logical_and(
mask_above_horizon,
mask_not_in_shade,
sun_horizon_position_series == None,
)
if numpy.any(mask_above_horizon_not_in_shade):
# sun_horizon_position_series[mask_above_horizon_not_in_shade] = [SunHorizonPositionModel.above.name]
logger.info(
f"Including positions of the sun above horizon and not in shade :\n{sun_horizon_position_series}",
alt=f"Including positions of the sun [bold yellow]above horizon[/bold yellow] and [bold red]not in shade[/bold red] :\n{sun_horizon_position_series}"
)
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"i Calculating the direct inclined irradiance for moments not in shade ..",
alt="i [bold]Calculating[/bold] the [magenta]direct inclined irradiance[/magenta] for moments not in shade .."
)
# if not read from external time series,
# will calculate the clear-sky index !
direct_inclined_irradiance_series = (
calculate_clear_sky_direct_inclined_irradiance_hofierka(
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
timestamps=timestamps,
timezone=timezone,
direct_horizontal_irradiance=direct_horizontal_irradiance, # FixMe
linke_turbidity_factor_series=linke_turbidity_factor_series,
apply_reflectivity_factor=apply_reflectivity_factor,
solar_incidence_series=solar_incidence_series,
solar_altitude_series=solar_altitude_series,
# solar_azimuth_series=solar_azimuth_series,
surface_in_shade_series=surface_in_shade_series,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
validate_output=validate_output,
verbose=verbose,
log=log,
)
)
direct_inclined_irradiance_series.build_output(
verbose=verbose, fingerprint=fingerprint
)
# Calculate diffuse and reflected irradiance for sun above horizon
if not numpy.any(mask_above_horizon):
logger.info(
"i [yellow bold]Apparently there is no moment of the sun above the horizon in the requested time series![/yellow bold] "
)
else:
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"i [bold]Calculating[/bold] the [magenta]diffuse inclined irradiance[/magenta] for daylight moments .."
)
diffuse_inclined_irradiance_series = (
calculate_clear_sky_diffuse_inclined_irradiance_muneer(
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
surface_tilt_horizontally_flat_panel_threshold=surface_tilt_horizontally_flat_panel_threshold,
timestamps=timestamps,
timezone=timezone,
global_horizontal_irradiance_series=global_horizontal_irradiance,
direct_horizontal_irradiance_series=direct_horizontal_irradiance,
linke_turbidity_factor_series=linke_turbidity_factor_series,
apply_reflectivity_factor=apply_reflectivity_factor,
solar_altitude_series=solar_altitude_series,
solar_azimuth_series=solar_azimuth_series,
solar_incidence_series=solar_incidence_series,
surface_in_shade_series=surface_in_shade_series,
shading_states=shading_states,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
)
diffuse_inclined_irradiance_series.build_output(
verbose=verbose, fingerprint=fingerprint
)
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"i [bold]Calculating[/bold] the [magenta]reflected inclined irradiance[/magenta] for daylight moments .."
)
ground_reflected_inclined_irradiance_series = calculate_ground_reflected_inclined_irradiance_series(
longitude=longitude,
latitude=latitude,
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
timestamps=timestamps,
timezone=timezone,
global_horizontal_irradiance=global_horizontal_irradiance, # time series, optional
linke_turbidity_factor_series=linke_turbidity_factor_series,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
albedo=albedo,
apply_reflectivity_factor=apply_reflectivity_factor,
solar_position_model=solar_position_model,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
fingerprint=fingerprint,
)
# sum components
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"\ni [bold]Calculating[/bold] the [magenta]global inclined irradiance[/magenta] .."
)
global_inclined_irradiance_series[mask_above_horizon_not_in_shade] += (
direct_inclined_irradiance_series.value[mask_above_horizon_not_in_shade]
)
global_inclined_irradiance_series[mask_above_horizon] += (
+ diffuse_inclined_irradiance_series.value[mask_above_horizon]
+ ground_reflected_inclined_irradiance_series.value[mask_above_horizon]
)
global_inclined_reflectivity_series = (
direct_inclined_irradiance_series.reflected
+ diffuse_inclined_irradiance_series.reflected
+ ground_reflected_inclined_irradiance_series.reflected
)
global_inclined_irradiance_before_reflectivity_series = (
direct_inclined_irradiance_series.value_before_reflectivity
+ diffuse_inclined_irradiance_series.value_before_reflectivity
+ ground_reflected_inclined_irradiance_series.value_before_reflectivity
)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=global_inclined_irradiance_series,
shape=timestamps.shape,
data_model=GlobalInclinedIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=global_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return GlobalInclinedIrradiance(
value=global_inclined_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
#
## Inclined Irradiance Components
direct_inclined_irradiance=direct_inclined_irradiance_series.value,
diffuse_inclined_irradiance=diffuse_inclined_irradiance_series.value,
ground_reflected_inclined_irradiance=ground_reflected_inclined_irradiance_series.value,
#
## Loss due to Reflectivity
reflected=global_inclined_reflectivity_series,
direct_inclined_reflected=direct_inclined_irradiance_series.reflected,
diffuse_inclined_reflected=diffuse_inclined_irradiance_series.reflected,
ground_reflected_inclined_reflected=ground_reflected_inclined_irradiance_series.reflected,
#
## Reflectivity Factor for Irradiance Components
direct_inclined_reflectivity_factor=direct_inclined_irradiance_series.reflectivity_factor,
diffuse_inclined_reflectivity_factor=diffuse_inclined_irradiance_series.reflectivity_factor,
ground_reflected_inclined_reflectivity_factor=ground_reflected_inclined_irradiance_series.reflectivity_factor,
#
## Reflectivity Coefficient which defines the Reflectivity Factor for Irradiance Components
# direct_inclined_reflectivity_coefficient=direct_inclined_reflectivity_coefficient_series,
diffuse_inclined_reflectivity_coefficient=diffuse_inclined_irradiance_series.reflectivity_coefficient,
# ground_reflected_inclined_reflectivity_coefficient=ground_reflected_inclined_reflectivity_coefficient_series,
#
## Inclined Irradiance before loss due to Reflectivity
value_before_reflectivity=global_inclined_irradiance_before_reflectivity_series,
direct_inclined_before_reflectivity=direct_inclined_irradiance_series.value_before_reflectivity,
diffuse_inclined_before_reflectivity=diffuse_inclined_irradiance_series.value_before_reflectivity,
ground_reflected_inclined_before_reflectivity=ground_reflected_inclined_irradiance_series.value_before_reflectivity,
#
## Horizontal Irradiance Components
global_horizontal_irradiance=global_horizontal_irradiance,
direct_horizontal_irradiance=direct_inclined_irradiance_series.direct_horizontal_irradiance,
diffuse_horizontal_irradiance=diffuse_inclined_irradiance_series.diffuse_horizontal_irradiance,
#
## Components of the diffuse sky-reflected irradiance
diffuse_sky_irradiance=diffuse_inclined_irradiance_series.diffuse_sky_irradiance,
term_n=diffuse_inclined_irradiance_series.term_n,
kb_ratio=diffuse_inclined_irradiance_series.kb_ratio,
#
## Components of the Extraterrestrial irradiance
extraterrestrial_horizontal_irradiance=diffuse_inclined_irradiance_series.extraterrestrial_horizontal_irradiance,
extraterrestrial_normal_irradiance=diffuse_inclined_irradiance_series.extraterrestrial_normal_irradiance,
linke_turbidity_factor=linke_turbidity_factor_series,
#
## Location and Position
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
sun_horizon_positions=sun_horizon_positions, # states != sun_horizon_position
#
## Solar Position parameters
surface_in_shade=surface_in_shade_series,
solar_incidence=direct_inclined_irradiance_series.solar_incidence,
shading_state=diffuse_inclined_irradiance_series.shading_state,
sun_horizon_position=sun_horizon_position_series, # positions != sun_horizon_positions
solar_altitude=solar_altitude_series,
refracted_solar_altitude=direct_inclined_irradiance_series.refracted_solar_altitude,
solar_azimuth=diffuse_inclined_irradiance_series.solar_azimuth,
# azimuth_difference=azimuth_difference_series,
#
## Positioning, Timing and Atmospheric algorithms
solar_positioning_algorithm=direct_inclined_irradiance_series.solar_positioning_algorithm,
solar_timing_algorithm=direct_inclined_irradiance_series.solar_timing_algorithm,
adjusted_for_atmospheric_refraction=solar_altitude_series.adjusted_for_atmospheric_refraction,
solar_incidence_model=direct_inclined_irradiance_series.solar_incidence_model,
solar_incidence_definition=direct_inclined_irradiance_series.solar_incidence.definition,
shading_algorithm=surface_in_shade_series.shading_algorithm,
shading_states=shading_states,
#
## Sources
data_source=HOFIERKA_2002,
solar_radiation_model=HOFIERKA_2002,
)
inclined ¶
API module to calculate the global (shortwave) irradiance over a location for a period in time.
Functions:
| Name | Description |
|---|---|
calculate_global_inclined_irradiance_hofierka | Calculate the global irradiance on an inclined surface [W.m-2] |
calculate_global_inclined_irradiance_hofierka ¶
calculate_global_inclined_irradiance_hofierka(
longitude: float,
latitude: float,
elevation: float,
surface_orientation: SurfaceOrientationModel = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTiltModel = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo | None = ZoneInfo("UTC"),
global_horizontal_irradiance: ndarray = array(
[], dtype=DATA_TYPE_DEFAULT
),
direct_horizontal_irradiance: ndarray = array(
[], dtype=DATA_TYPE_DEFAULT
),
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
albedo: float | None = ALBEDO_DEFAULT,
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = None,
solar_incidence_series: SolarIncidence | None = None,
solar_position_model: SolarPositionModel = noaa,
surface_in_shade_series: NpNDArray | None = None,
sun_horizon_position: List[
SunHorizonPositionModel
] = SUN_HORIZON_POSITION_DEFAULT,
shading_states: List[ShadingState] = [all],
solar_time_model: SolarTimeModel = noaa,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
)
Calculate the global irradiance on an inclined surface [W.m-2]
Calculate the global irradiance on an inclined surface as the sum of the direct, the diffuse and the ground-reflected radiation components. The radiation, selectively attenuated by the atmosphere, which is not reflected or scattered and reaches the surface directly is the direct radiation. The scattered radiation that reaches the ground is the diffuse radiation. In addition, a smaller part of radiation is reflected from the ground onto the inclined surface. Only small percents of reflected radiation contribute to inclined surfaces, thus it is sometimes ignored. PVGIS, however, inherits the solutions adopted in the r.sun solar radiation model in which both the diffuse and reflected radiation components are considered.
Source code in pvgisprototype/algorithms/hofierka/irradiance/shortwave/inclined.py
@log_function_call
@custom_cached
def calculate_global_inclined_irradiance_hofierka(
longitude: float,
latitude: float,
elevation: float,
surface_orientation: SurfaceOrientationModel = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTiltModel = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo | None = ZoneInfo("UTC"),
global_horizontal_irradiance: ndarray = numpy.array([], dtype=DATA_TYPE_DEFAULT),
direct_horizontal_irradiance: ndarray = numpy.array([], dtype=DATA_TYPE_DEFAULT),
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
# unrefracted_solar_zenith: UnrefractedSolarZenith | None = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT, # radians
albedo: float | None = ALBEDO_DEFAULT,
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = None,
solar_incidence_series: SolarIncidence | None = None,
solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
surface_in_shade_series: NpNDArray | None = None,
sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
shading_states: List[ShadingState] = [ShadingState.all],
solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output:bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
):
"""Calculate the global irradiance on an inclined surface [W.m-2]
Calculate the global irradiance on an inclined surface as the sum of the
direct, the diffuse and the ground-reflected radiation components.
The radiation, selectively attenuated by the atmosphere, which is not
reflected or scattered and reaches the surface directly is the direct
radiation. The scattered radiation that reaches the ground is the
diffuse radiation. In addition, a smaller part of radiation is reflected
from the ground onto the inclined surface. Only small percents of reflected
radiation contribute to inclined surfaces, thus it is sometimes ignored.
PVGIS, however, inherits the solutions adopted in the r.sun solar radiation
model in which both the diffuse and reflected radiation components are
considered.
"""
# In order to avoid unbound errors we pre-define `_series` objects
array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from timestamps
zero_array = create_array(**array_parameters)
extended_array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"backend": array_backend,
}
unset_series = create_array(**extended_array_parameters, init_method="unset")
# # direct
# # ----------------------------------------------- Important !
# if direct_horizontal_irradiance is None:
# direct_horizontal_irradiance = zero_array
# # Important ! -----------------------------------------------
direct_inclined_irradiance_series = DirectInclinedIrradiance(
value=zero_array,
solar_incidence=SolarIncidence(value=unset_series),
)
direct_inclined_irradiance_series.reflected = zero_array
direct_inclined_irradiance_series.value_before_reflectivity = zero_array
direct_inclined_irradiance_series.reflectivity_factor = zero_array
direct_normal_irradiance = DirectNormalIrradiance(
value=zero_array,
# extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance,
)
direct_inclined_irradiance_series.direct_horizontal_irradiance = (
DirectHorizontalIrradianceFromExternalData(
value=direct_horizontal_irradiance,
direct_normal_irradiance=direct_normal_irradiance,
)
)
# direct_inclined_irradiance_series.solar_incidence_model = solar_incidence_model
# # direct_inclined_irradiance_series.solar_incidence.definition =
no_earth_orbit = {
'eccentricity_phase_offset': None,
'eccentricity_amplitude': None,
}
# extraterrestrial normal to be fed indirectly to diffuse_inclined_irradiance
extraterrestrial_normal_irradiance_series = ExtraterrestrialNormalIrradiance(
value=unset_series,
unit=NOT_AVAILABLE,
day_angle=unset_series,
solar_constant=None,
**no_earth_orbit,
distance_correction_factor=None,
)
extraterrestrial_horizontal_irradiance_series = ExtraterrestrialHorizontalIrradiance(
value=unset_series,
unit=NOT_AVAILABLE,
day_angle=unset_series,
solar_constant=None,
**no_earth_orbit,
distance_correction_factor=None,
)
# diffuse sky-reflected
diffuse_inclined_irradiance_series = DiffuseSkyReflectedInclinedIrradiance(
value=zero_array,
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
extraterrestrial_horizontal_irradiance=extraterrestrial_horizontal_irradiance_series,
solar_azimuth=SolarAzimuth(value=unset_series),
# direct_horizontal_irradiance=direct_horizontal_irradiance,
)
diffuse_inclined_irradiance_series.reflected = zero_array
diffuse_inclined_irradiance_series.value_before_reflectivity = zero_array
diffuse_inclined_irradiance_series.reflectivity_factor = zero_array
diffuse_inclined_irradiance_series.diffuse_horizontal_irradiance = DiffuseSkyReflectedHorizontalIrradianceFromExternalData(value=zero_array)
# diffuse ground-reflected
# note : there is no ground-reflected _horizontal_ component as such !
ground_reflected_inclined_irradiance_series = DiffuseGroundReflectedInclinedIrradiance(
value=zero_array
)
ground_reflected_inclined_irradiance_series.reflected = zero_array
ground_reflected_inclined_irradiance_series.value_before_reflectivity = zero_array
ground_reflected_inclined_irradiance_series.reflectivity_factor = zero_array
global_inclined_irradiance_series = zero_array
# Select which solar positions related to the horizon to process
sun_horizon_positions = select_models(
SunHorizonPositionModel, sun_horizon_position
) # Using a Typer callback fails !
# and keep track of the position of the sun relative to the horizon
sun_horizon_position_series = create_array(
timestamps.shape, dtype="object", init_method="empty", backend=array_backend
)
# Following, create masks based on the solar altitude series --------
# For sun below the horizon
if SunHorizonPositionModel.below in sun_horizon_positions:
mask_below_horizon = solar_altitude_series.value < 0
sun_horizon_position_series[mask_below_horizon] = [SunHorizonPositionModel.below.value]
if numpy.any(mask_below_horizon):
logger.info(
f"Positions of the sun below horizon :\n{sun_horizon_position_series}",
alt=f"Positions of the sun [bold gray50]below horizon[/bold gray50] :\n{sun_horizon_position_series}"
)
# no incident radiance without direct sunlight !
direct_inclined_irradiance_series.value[mask_below_horizon] = 0
diffuse_inclined_irradiance_series.value[mask_below_horizon] = 0
ground_reflected_inclined_irradiance_series.value[mask_below_horizon] = 0
# For very low sun angles
if SunHorizonPositionModel.low_angle in sun_horizon_positions:
mask_low_angle = numpy.logical_and(
solar_altitude_series.value >= 0,
solar_altitude_series.value < solar_altitude_series.low_angle_threshold_radians, # attribute in SolarAltitude data model
sun_horizon_position_series == None, # operate only on unset elements
)
sun_horizon_position_series[mask_low_angle] = [
SunHorizonPositionModel.low_angle.value
]
direct_inclined_irradiance_series.value[mask_low_angle] = (
0 # Direct radiation is negligible
)
# For sun above the horizon
if SunHorizonPositionModel.above in sun_horizon_positions:
mask_above_horizon = numpy.logical_and(
solar_altitude_series.value > 0,
sun_horizon_position_series == None, # operate only on unset elements
)
sun_horizon_position_series[mask_above_horizon] = [
SunHorizonPositionModel.above.value
]
# For sun above horizon and not in shade
mask_not_in_shade = ~surface_in_shade_series.value
mask_above_horizon_not_in_shade = numpy.logical_and(
mask_above_horizon,
mask_not_in_shade,
sun_horizon_position_series == None,
)
if numpy.any(mask_above_horizon_not_in_shade):
# sun_horizon_position_series[mask_above_horizon_not_in_shade] = [SunHorizonPositionModel.above.name]
logger.info(
f"Including positions of the sun above horizon and not in shade :\n{sun_horizon_position_series}",
alt=f"Including positions of the sun [bold yellow]above horizon[/bold yellow] and [bold red]not in shade[/bold red] :\n{sun_horizon_position_series}"
)
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"i Calculating the direct inclined irradiance for moments not in shade ..",
alt="i [bold]Calculating[/bold] the [magenta]direct inclined irradiance[/magenta] for moments not in shade .."
)
# if not read from external time series,
# will calculate the clear-sky index !
direct_inclined_irradiance_series = calculate_direct_inclined_irradiance_hofierka(
timestamps=timestamps,
timezone=timezone,
direct_horizontal_irradiance=direct_horizontal_irradiance,
apply_reflectivity_factor=apply_reflectivity_factor,
solar_incidence_series=solar_incidence_series,
solar_altitude_series=solar_altitude_series,
# solar_azimuth_series=solar_azimuth_series,
surface_in_shade_series=surface_in_shade_series,
dtype=dtype,
array_backend=array_backend,
validate_output=validate_output,
verbose=verbose,
log=log,
)
direct_inclined_irradiance_series.build_output(
verbose=verbose, fingerprint=fingerprint
)
# Calculate diffuse and reflected irradiance for sun above horizon
if not numpy.any(mask_above_horizon):
logger.info(
"i [yellow bold]Apparently there is no moment of the sun above the horizon in the requested time series![/yellow bold] "
)
else:
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"i [bold]Calculating[/bold] the [magenta]diffuse inclined irradiance[/magenta] for daylight moments .."
)
diffuse_inclined_irradiance_series = (
calculate_diffuse_inclined_irradiance_muneer(
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
surface_tilt_horizontally_flat_panel_threshold=surface_tilt_horizontally_flat_panel_threshold,
timestamps=timestamps,
timezone=timezone,
global_horizontal_irradiance_series=global_horizontal_irradiance,
direct_horizontal_irradiance_series=direct_horizontal_irradiance,
apply_reflectivity_factor=apply_reflectivity_factor,
solar_altitude_series=solar_altitude_series,
solar_azimuth_series=solar_azimuth_series,
solar_incidence_series=solar_incidence_series,
surface_in_shade_series=surface_in_shade_series,
shading_states=shading_states,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
)
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"i [bold]Calculating[/bold] the [magenta]reflected inclined irradiance[/magenta] for daylight moments .."
)
ground_reflected_inclined_irradiance_series = calculate_ground_reflected_inclined_irradiance_series(
longitude=longitude,
latitude=latitude,
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
timestamps=timestamps,
timezone=timezone,
global_horizontal_irradiance=global_horizontal_irradiance, # time series, optional
linke_turbidity_factor_series=linke_turbidity_factor_series,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
albedo=albedo,
apply_reflectivity_factor=apply_reflectivity_factor,
solar_position_model=solar_position_model,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
fingerprint=fingerprint,
)
# sum components
if verbose > HASH_AFTER_THIS_VERBOSITY_LEVEL:
logger.info(
"\ni [bold]Calculating[/bold] the [magenta]global inclined irradiance[/magenta] .."
)
global_inclined_irradiance_series[mask_above_horizon_not_in_shade] += (
direct_inclined_irradiance_series.value[mask_above_horizon_not_in_shade]
)
global_inclined_irradiance_series[mask_above_horizon] += (
+ diffuse_inclined_irradiance_series.value[mask_above_horizon]
+ ground_reflected_inclined_irradiance_series.value[mask_above_horizon]
)
global_inclined_reflected_series = (
direct_inclined_irradiance_series.reflected
+ diffuse_inclined_irradiance_series.reflected
+ ground_reflected_inclined_irradiance_series.reflected
)
global_inclined_irradiance_before_reflectivity_series = (
direct_inclined_irradiance_series.value_before_reflectivity
+ diffuse_inclined_irradiance_series.value_before_reflectivity
+ ground_reflected_inclined_irradiance_series.value_before_reflectivity
)
out_of_range, out_of_range_index = identify_values_out_of_range(
series=global_inclined_irradiance_series,
shape=timestamps.shape,
data_model=GlobalInclinedIrradianceFromExternalData(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=global_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
# We need to build this for the returned data model !
diffuse_inclined_irradiance_series.build_output(
verbose=verbose, fingerprint=fingerprint
)
return GlobalInclinedIrradianceFromExternalData(
value=global_inclined_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
#
## Inclined Irradiance Components
direct_inclined_irradiance=direct_inclined_irradiance_series.value,
diffuse_inclined_irradiance=diffuse_inclined_irradiance_series.value,
ground_reflected_inclined_irradiance=ground_reflected_inclined_irradiance_series.value,
#
## Loss due to Reflectivity
reflected=global_inclined_reflected_series,
direct_inclined_reflected=direct_inclined_irradiance_series.reflected,
diffuse_inclined_reflected=diffuse_inclined_irradiance_series.reflected,
ground_reflected_inclined_reflected=ground_reflected_inclined_irradiance_series.reflected,
#
## Reflectivity Factor for Irradiance Components
direct_inclined_reflectivity_factor=direct_inclined_irradiance_series.reflectivity_factor,
diffuse_inclined_reflectivity_factor=diffuse_inclined_irradiance_series.reflectivity_factor,
ground_reflected_inclined_reflectivity_factor=ground_reflected_inclined_irradiance_series.reflectivity_factor,
#
## Reflectivity Coefficient which defines the Reflectivity Factor for Irradiance Components
# direct_inclined_reflectivity_coefficient=direct_inclined_reflectivity_coefficient_series,
diffuse_inclined_reflectivity_coefficient=diffuse_inclined_irradiance_series.reflectivity_coefficient,
# ground_reflected_inclined_reflectivity_coefficient=ground_reflected_inclined_reflectivity_coefficient_series,
#
## Inclined Irradiance before loss due to Reflectivity
value_before_reflectivity=global_inclined_irradiance_before_reflectivity_series,
direct_inclined_before_reflectivity=direct_inclined_irradiance_series.value_before_reflectivity,
diffuse_inclined_before_reflectivity=diffuse_inclined_irradiance_series.value_before_reflectivity,
ground_reflected_inclined_before_reflectivity=ground_reflected_inclined_irradiance_series.value_before_reflectivity,
#
## Horizontal Irradiance Components
global_horizontal_irradiance=global_horizontal_irradiance,
direct_horizontal_irradiance=direct_inclined_irradiance_series.direct_horizontal_irradiance,
diffuse_horizontal_irradiance=diffuse_inclined_irradiance_series.diffuse_horizontal_irradiance,
#
## Components of the diffuse sky-reflected irradiance
diffuse_sky_irradiance=diffuse_inclined_irradiance_series.diffuse_sky_irradiance,
term_n=diffuse_inclined_irradiance_series.term_n,
kb_ratio=diffuse_inclined_irradiance_series.kb_ratio,
#
## Components of the Extraterrestrial irradiance
extraterrestrial_horizontal_irradiance=diffuse_inclined_irradiance_series.extraterrestrial_horizontal_irradiance,
extraterrestrial_normal_irradiance=diffuse_inclined_irradiance_series.extraterrestrial_normal_irradiance,
linke_turbidity_factor=linke_turbidity_factor_series,
#
## Location and Position
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
sun_horizon_positions=sun_horizon_positions, # states != sun_horizon_position
#
## Solar Position parameters
surface_in_shade=surface_in_shade_series,
solar_incidence=direct_inclined_irradiance_series.solar_incidence,
shading_state=diffuse_inclined_irradiance_series.shading_state,
sun_horizon_position=sun_horizon_position_series, # positions != sun_horizon_positions
solar_altitude=solar_altitude_series,
refracted_solar_altitude=direct_inclined_irradiance_series.refracted_solar_altitude,
solar_azimuth=diffuse_inclined_irradiance_series.solar_azimuth,
# azimuth_difference=azimuth_difference_series,
#
## Positioning, Timing and Atmospheric algorithms
solar_positioning_algorithm=direct_inclined_irradiance_series.solar_positioning_algorithm,
solar_timing_algorithm=direct_inclined_irradiance_series.solar_timing_algorithm,
adjusted_for_atmospheric_refraction=solar_altitude_series.adjusted_for_atmospheric_refraction,
solar_incidence_model=direct_inclined_irradiance_series.solar_incidence_model,
solar_incidence_definition=direct_inclined_irradiance_series.solar_incidence.definition,
shading_algorithm=surface_in_shade_series.shading_algorithm,
shading_states=shading_states,
#
## Sources
data_source=HOFIERKA_2002,
solar_radiation_model=HOFIERKA_2002,
)
position ¶
Modules:
| Name | Description |
|---|---|
event_hour_angle | |
fractional_year | |
shading | |
solar_altitude | |
solar_azimuth | |
solar_declination | |
solar_hour_angle | |
solar_incidence | |
event_hour_angle ¶
Functions:
| Name | Description |
|---|---|
calculate_event_hour_angle_pvis | Calculate the hour angle (ω) at sunrise and sunset |
calculate_event_hour_angle_pvis ¶
calculate_event_hour_angle_pvis(
latitude: Latitude,
surface_tilt: float = 0,
solar_declination: float = 0,
) -> EventHourAngle
Calculate the hour angle (ω) at sunrise and sunset
Hour angle = acos(-tan(Latitude Angle-Tilt Angle)*tan(Declination Angle))
The hour angle (ω) at sunrise and sunset measures the angular distance between the sun at the local solar time and the sun at solar noon.
ω = acos(-tan(Φ-β)*tan(δ))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
latitude | Latitude | Latitude (Φ) is the angle between the sun's rays and its projection on the horizontal surface measured in radians | required |
surface_tilt | float | Surface tilt (or slope) (β) is the angle between the inclined surface (slope) and the horizontal plane. | 0 |
solar_declination | float | Solar declination (δ) is the angle between the equator and a line drawn from the centre of the Earth to the centre of the sun measured in radians. | 0 |
Returns:
| Name | Type | Description |
|---|---|---|
hour_angle_sunrise | float | Hour angle (ω) is the angle at any instant through which the earth has to turn to bring the meridian of the observer directly in line with the sun's rays measured in radian. |
Source code in pvgisprototype/algorithms/hofierka/position/event_hour_angle.py
@log_function_call
@custom_cached
def calculate_event_hour_angle_pvis(
latitude: Latitude,
surface_tilt: float = 0,
solar_declination: float = 0,
) -> EventHourAngle:
"""Calculate the hour angle (ω) at sunrise and sunset
Hour angle = acos(-tan(Latitude Angle-Tilt Angle)*tan(Declination Angle))
The hour angle (ω) at sunrise and sunset measures the angular distance
between the sun at the local solar time and the sun at solar noon.
ω = acos(-tan(Φ-β)*tan(δ))
Parameters
----------
latitude: float
Latitude (Φ) is the angle between the sun's rays and its projection on the
horizontal surface measured in radians
surface_tilt: float
Surface tilt (or slope) (β) is the angle between the inclined surface
(slope) and the horizontal plane.
solar_declination: float
Solar declination (δ) is the angle between the equator and a line drawn
from the centre of the Earth to the centre of the sun measured in
radians.
Returns
-------
hour_angle_sunrise: float
Hour angle (ω) is the angle at any instant through which the earth has
to turn to bring the meridian of the observer directly in line with the
sun's rays measured in radian.
"""
hour_angle_sunrise_value = arccos(
-arctan(latitude - surface_tilt) * arctan(solar_declination)
)
return EventHourAngle(
value=hour_angle_sunrise_value,
unit=RADIANS,
)
fractional_year ¶
Functions:
| Name | Description |
|---|---|
calculate_day_angle_series_hofierka | Calculate the day angle for a time series. |
calculate_day_angle_series_hofierka ¶
calculate_day_angle_series_hofierka(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> FractionalYear
Calculate the day angle for a time series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamps | DatetimeIndex | A Pandas DatetimeIndex representing the timestamps. | required |
dtype | str | The data type for the calculations (the default is 'float32'). | DATA_TYPE_DEFAULT |
array_backend | str | The backend used for calculations (the default is 'NUMPY'). | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level | VERBOSE_LEVEL_DEFAULT |
log | int | Log level | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
FractionalYear | A FractionalYear object containing the calculated fractional year series. |
Notes
The day angle [0]_ is the same quantity called Fractional Year in NOAA's solar geometry equations, i.e. the function calculate_fractional_year_time_series_noaa().
In PVGIS' C source code, this is called day_angle
NOAA's corresponding equation:
fractional_year = (
2
* pi
/ 365
* (timestamp.timetuple().tm_yday - 1 + float(timestamp.hour - 12) / 24)
)
References
.. [0] Hofierka, 2002
NOAA's corresponding equation:
day_angle = (
2
* pi
/ 365
* (timestamp.timetuple().tm_yday - 1 + float(timestamp.hour - 12) / 24)
)
Source code in pvgisprototype/algorithms/hofierka/position/fractional_year.py
@log_function_call
@custom_cached
def calculate_day_angle_series_hofierka(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> FractionalYear:
"""Calculate the day angle for a time series.
Parameters
----------
timestamps : DatetimeIndex
A Pandas DatetimeIndex representing the timestamps.
dtype : str, optional
The data type for the calculations (the default is 'float32').
array_backend : str, optional
The backend used for calculations (the default is 'NUMPY').
verbose: int
Verbosity level
log: int
Log level
Returns
-------
FractionalYear
A FractionalYear object containing the calculated fractional year
series.
Notes
-----
The day angle [0]_ is the same quantity called Fractional Year in NOAA's
solar geometry equations, i.e. the function
calculate_fractional_year_time_series_noaa().
In PVGIS' C source code, this is called `day_angle`
NOAA's corresponding equation:
fractional_year = (
2
* pi
/ 365
* (timestamp.timetuple().tm_yday - 1 + float(timestamp.hour - 12) / 24)
)
References
----------
.. [0] Hofierka, 2002
NOAA's corresponding equation:
day_angle = (
2
* pi
/ 365
* (timestamp.timetuple().tm_yday - 1 + float(timestamp.hour - 12) / 24)
)
"""
days_of_year = timestamps.dayofyear
days_in_years = get_days_in_years(timestamps.year)
array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from timestamps
day_angle_series = create_array(**array_parameters)
day_angle_series = numpy.array(
2 * pi * days_of_year / days_in_years,
dtype=dtype,
)
if not numpy.all(
(FractionalYear().min_radians <= day_angle_series)
& (day_angle_series <= FractionalYear().max_radians)
):
index_of_out_of_range_values = numpy.where(
(day_angle_series < FractionalYear().min_radians)
| (day_angle_series > FractionalYear().max_radians)
)
out_of_range_values = day_angle_series[index_of_out_of_range_values]
# Report values in "human readable" degrees
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{FractionalYear().min_radians}, {FractionalYear().max_radians}] radians"
f" in [code]day_angle_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=day_angle_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return FractionalYear(
value=day_angle_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.hofierka,
)
shading ¶
Functions:
| Name | Description |
|---|---|
calculate_horizon_height_series | |
calculate_surface_in_shade_series_pvgis | Determine a time series for when a location of observation (or else in |
calculate_horizon_height_series ¶
calculate_horizon_height_series(
solar_azimuth_series: SolarAzimuth,
horizon_profile: DataArray | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> HorizonHeight
Source code in pvgisprototype/algorithms/hofierka/position/shading.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateHorizonHeightSeriesInputModel)
def calculate_horizon_height_series(
solar_azimuth_series: SolarAzimuth,
horizon_profile: DataArray | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> HorizonHeight:
""" """
# Ensure _all_ azimuth values are measured in radians !
# Tolerance for matching azimuths (set this based on acceptable precision)
# tolerance = finfo(float).eps # Machine epsilon for floating point comparison
# or :
# tolerance = 0.001 # or a smaller value, depending on your precision needs
# ?
# select closest azimuth values using Xarray's `sel` with `method="nearest"`
# closest_horizon_heights = horizon_profile.sel(azimuth=solar_azimuth_series.radians, method="nearest", tolerance=tolerance)
# # Identify where the tolerance is exceeded (i.e., where interpolation is needed)
# azimuth_difference = abs(closest_horizon_heights.azimuth - solar_azimuth_series.radians)
# needs_interpolation = azimuth_difference > tolerance
# We need the index of the selected values !
# # Assign exact or almost exact matches from the horizon profile
# close_enough = ~needs_interpolation
# horizon_height_series[close_enough] = horizon_profile.sel(azimuth=solar_azimuth_series.radians[close_enough])
# # Interpolate values not close enough
# import numpy
# if numpy.any(needs_interpolation):
# horizon_height_series[needs_interpolation] = horizon_profile.interp(
# azimuth=solar_azimuth_series.radians[needs_interpolation]
# )
if isinstance(horizon_profile, DataArray):
if (horizon_profile == 0).all():
from pvgisprototype.core.arrays import create_array
# If all values are zero, assume flat terrain
array_parameters = {
"shape": solar_azimuth_series.value.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from solar_azimuth_series
horizon_height_series = create_array(**array_parameters)
else:
max_horizon_azimuth = horizon_profile.values.max().item()
max_solar_azimuth = solar_azimuth_series.value.max().item()
if max_solar_azimuth > max_horizon_azimuth:
# Before interpollate, append one more pair of solar azimuth at 360 degrees (or 2*pi) and
# repeat the 0 degrees solar azimuth value at the 360 degrees solar azimuth
from pvgisprototype.constants import pi
import xarray as xr
new_max_horizon_azimuth = 2 * pi
last_horizon_height_value = horizon_profile[0].values
new_point = xr.DataArray(
[last_horizon_height_value],
dims="azimuth",
coords={"azimuth": [new_max_horizon_azimuth]},
)
horizon_profile = xr.concat([horizon_profile, new_point], dim="azimuth")
horizon_height_series = horizon_profile.interp(
azimuth=solar_azimuth_series.radians,
).values # retrieve the NumPy array of values here !
else: # we assume a flat terrain
from pvgisprototype.core.arrays import create_array
array_parameters = {
"shape": solar_azimuth_series.value.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from solar_azimuth_series
horizon_height_series = create_array(**array_parameters)
return HorizonHeight(value=horizon_height_series, unit=RADIANS)
calculate_surface_in_shade_series_pvgis ¶
calculate_surface_in_shade_series_pvgis(
solar_altitude_series: SolarAltitude,
solar_azimuth_series: SolarAzimuth,
horizon_profile: DataArray | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> LocationShading
Determine a time series for when a location of observation (or else in the context of photovoltaic analysis a solar surface) is in shade.
The function compares time series of solar altitude and horizon height, the latter measured at directions corresponding to the solar azimuth series. The comparison derives a qualitative series of the sun being visible from the location of observation or indeed behind the horizon.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solar_altitude_series_array | Array of solar altitude angles for each timestamp. | required |
Returns:
| Type | Description |
|---|---|
NumPy array: Boolean array indicating whether the surface is in shade at | |
each timestamp. | |
Source code in pvgisprototype/algorithms/hofierka/position/shading.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSurfaceInShadePVGISInputModel)
def calculate_surface_in_shade_series_pvgis(
solar_altitude_series: SolarAltitude,
solar_azimuth_series: SolarAzimuth,
horizon_profile: DataArray | None = None,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> LocationShading:
"""Determine a time series for when a location of observation (or else in
the context of photovoltaic analysis a solar surface) is in shade.
The function compares time series of solar altitude and horizon height, the
latter measured at directions corresponding to the solar azimuth series.
The comparison derives a qualitative series of the sun being visible from
the location of observation or indeed behind the horizon.
Parameters
----------
solar_altitude_series_array: numpy array
Array of solar altitude angles for each timestamp.
Returns
-------
NumPy array: Boolean array indicating whether the surface is in shade at
each timestamp.
"""
horizon_height_series = calculate_horizon_height_series(
solar_azimuth_series=solar_azimuth_series,
horizon_profile=horizon_profile,
dtype=dtype,
array_backend=array_backend,
validate_output=validate_output,
verbose=verbose,
log=log,
)
horizon_height_series.build_output(verbose=verbose, fingerprint=None)
surface_in_shade_series = where(
solar_altitude_series.value < horizon_height_series.value, True, False
)
logger.debug(f"In shade : {surface_in_shade_series}")
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=surface_in_shade_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return LocationShading(
value=surface_in_shade_series,
unit=UNITLESS,
solar_altitude=solar_altitude_series.value,
solar_azimuth=solar_azimuth_series.value,
horizon_height=horizon_height_series,
visible=~surface_in_shade_series,
shading_algorithm="PVGIS",
solar_positioning_algorithm=solar_altitude_series.solar_positioning_algorithm,
solar_timing_algorithm=solar_altitude_series.solar_timing_algorithm,
)
solar_altitude ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_altitude_series_hofierka | Calculate the solar altitude angle. |
calculate_solar_altitude_series_hofierka ¶
calculate_solar_altitude_series_hofierka(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
eccentricity_phase_offset: float,
eccentricity_amplitude: float,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAltitude
Calculate the solar altitude angle.
Calculate the solar altitude angle based on the equation
sine_solar_altitude = ( sin(latitude.radians) * np.sin(solar_declination_series.radians) + cos(latitude.radians) * np.cos(solar_declination_series.radians) * np.cos(solar_hour_angle_series.radians)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | The longitude in degrees. This value will be converted to radians. It should be in the range [-180, 180]. | required |
latitude | float | The latitude in degrees. This value will be converted to radians. It should be in the range [-90, 90]. | required |
timestamp | datetime | The timestamp for which to calculate the solar altitude. If not provided, the current UTC time will be used. | required |
timezone | str | The timezone to use for the calculation. If not provided, the system's local timezone will be used. | required |
Returns:
| Type | Description |
|---|---|
float | The calculated solar altitude. |
Notes
NOAA's equation is practically the same, though it targets the cosine function of the solar zenith angle which it the complementary of the solar altitude angle.
cosine_solar_zenith = ( sin(latitude.radians) * np.sin(solar_declination_series.radians) + cos(latitude.radians) * np.cos(solar_declination_series.radians) * np.cos(solar_hour_angle_series.radians)
Source code in pvgisprototype/algorithms/hofierka/position/solar_altitude.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarAltitudePVISInputModel)
def calculate_solar_altitude_series_hofierka(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
eccentricity_phase_offset: float,
eccentricity_amplitude: float,
# solar_time_model: SolarTimeModel,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAltitude:
"""Calculate the solar altitude angle.
Calculate the solar altitude angle based on the equation
sine_solar_altitude = (
sin(latitude.radians)
* np.sin(solar_declination_series.radians)
+ cos(latitude.radians)
* np.cos(solar_declination_series.radians)
* np.cos(solar_hour_angle_series.radians)
Parameters
----------
longitude : float
The longitude in degrees. This value will be converted to radians.
It should be in the range [-180, 180].
latitude : float
The latitude in degrees. This value will be converted to radians.
It should be in the range [-90, 90].
timestamp : datetime, optional
The timestamp for which to calculate the solar altitude.
If not provided, the current UTC time will be used.
timezone : str, optional
The timezone to use for the calculation.
If not provided, the system's local timezone will be used.
Returns
-------
float
The calculated solar altitude.
Notes
-----
NOAA's equation is practically the same, though it targets the cosine
function of the solar zenith angle which it the complementary of the solar
altitude angle.
cosine_solar_zenith = (
sin(latitude.radians)
* np.sin(solar_declination_series.radians)
+ cos(latitude.radians)
* np.cos(solar_declination_series.radians)
* np.cos(solar_hour_angle_series.radians)
"""
solar_declination_series = calculate_solar_declination_series_hofierka(
timestamps=timestamps,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
# Idea for alternative solar time modelling, i.e. Milne 1921 -------------
# solar_time = model_solar_time(
# longitude=longitude,
# latitude=latitude,
# timestamp=timestamp,
# timezone=timezone,
# solar_time_model=solar_time_model, # returns datetime.time object
# eccentricity_phase_offset=eccentricity_phase_offset,
# eccentricity_amplitude=eccentricity_amplitude,
# )
# hour_angle = calculate_solar_hour_angle_pvis(
# solar_time=solar_time,
# )
# ------------------------------------------------------------------------
solar_hour_angle_series = calculate_solar_hour_angle_series_hofierka(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
C31 = cos(latitude.radians) * numpy.cos(solar_declination_series.radians)
C33 = sin(latitude.radians) * numpy.sin(solar_declination_series.radians)
sine_solar_altitude_series = C31 * numpy.cos(solar_hour_angle_series.radians) + C33
solar_altitude_series = numpy.arcsin(sine_solar_altitude_series)
# mask_positive_C31 = C31 > 1e-7
# solar_altitude_series[mask_positive_C31] = numpy.where(
# sine_solar_altitude_series < 0,
# NO_SOLAR_INCIDENCE,
# solar_altitude_series,
# )
# The hour angle of the time of sunrise/sunset over a horizontal surface
# Thr,s can be calculated then as:
# cos(event_hour_angle_horizontal) = -C33 / C31
if (
(solar_altitude_series < SolarAltitude().min_radians)
| (solar_altitude_series > SolarAltitude().max_radians)
).any():
out_of_range_values = solar_altitude_series[
(solar_altitude_series < SolarAltitude().min_radians)
| (solar_altitude_series > SolarAltitude().max_radians)
]
# raise ValueError(# ?
logger.warning(
f"{WARNING_NEGATIVE_VALUES} "
f"[{SolarAltitude().min_radians}, {SolarAltitude().max_radians}] radians"
f" in [code]solar_altitude_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_altitude_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAltitude(
value=solar_altitude_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.hofierka,
solar_timing_algorithm=solar_hour_angle_series.solar_timing_algorithm,
)
solar_azimuth ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_azimuth_series_hofierka | Calculate the solar azimuth angle |
calculate_solar_azimuth_series_hofierka ¶
calculate_solar_azimuth_series_hofierka(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAzimuth
Calculate the solar azimuth angle
Returns:
| Name | Type | Description |
|---|---|---|
solar_azimuth | float | |
Returns:
| Name | Type | Description |
|---|---|---|
solar_azimuth | float | |
Notes
According to Hofierka! solar azimuth is measured from East! Conflict with Jenvco 1992?
Source code in pvgisprototype/algorithms/hofierka/position/solar_azimuth.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarAzimuthPVISInputModel)
def calculate_solar_azimuth_series_hofierka(
longitude: Longitude, # radians
latitude: Latitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAzimuth:
"""Calculate the solar azimuth angle
Returns
-------
solar_azimuth: float
Returns
-------
solar_azimuth: float
Notes
-----
According to Hofierka! solar azimuth is measured from East!
Conflict with Jenvco 1992?
"""
solar_declination_series = calculate_solar_declination_series_hofierka(
timestamps=timestamps,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
# Idea for alternative solar time modelling, i.e. Milne 1921 -------------
# solar_time = model_solar_time(
# longitude=longitude,
# latitude=latitude,
# timestamp=timestamp,
# timezone=timezone,
# solar_time_model=solar_time_model, # returns datetime.time object
# eccentricity_phase_offset=eccentricity_phase_offset,
# eccentricity_amplitude=eccentricity_amplitude,
# )
# hour_angle = calculate_solar_hour_angle_pvis(
# solar_time=solar_time,
# )
# ------------------------------------------------------------------------
solar_hour_angle_series = calculate_solar_hour_angle_series_hofierka(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
C11 = sin(latitude.radians) * numpy.cos(solar_declination_series.radians)
C13 = -cos(latitude.radians) * numpy.sin(
solar_declination_series.radians
) # Attention to the - sign
C22 = numpy.cos(solar_declination_series.radians)
x_solar_vector_component = -C22 * numpy.sin(solar_hour_angle_series.radians)
y_solar_vector_component = C11 * numpy.cos(solar_hour_angle_series.radians) + C13
# numerator = y_solar_vector_component
denominator_a = numpy.power(x_solar_vector_component, 2)
denominator_b = numpy.power(y_solar_vector_component, 2)
event_hour_angle_inclined = numpy.power(denominator_a + denominator_b, 0.5)
# mask_positive_event_hour_angle_inclined = event_hour_angle_inclined > 1e-7
mask_negative_event_hour_angle_inclined = event_hour_angle_inclined < 1e-7 # ?
azimuth_origin = "East"
cosine_solar_azimuth_series = numpy.clip(
y_solar_vector_component / event_hour_angle_inclined, -1, 1
)
solar_azimuth_series = numpy.arccos(cosine_solar_azimuth_series)
solar_azimuth_series = numpy.where(
x_solar_vector_component < 0,
numpy.pi * 2 - solar_azimuth_series,
solar_azimuth_series,
)
solar_azimuth_series = numpy.where(
mask_negative_event_hour_angle_inclined,
NO_SOLAR_INCIDENCE,
solar_azimuth_series,
)
from math import pi
solar_azimuth_series = numpy.where(
solar_azimuth_series < pi / 2,
pi / 2 - solar_azimuth_series,
5 * pi / 2 - solar_azimuth_series,
)
solar_azimuth_series = numpy.where(
solar_azimuth_series >= 2 * pi,
solar_azimuth_series - 2 * pi,
solar_azimuth_series,
)
# -------------------------- convert east to north zero degrees convention
# PVGIS' follows Hofierka (2002) who states : azimuth is measured from East
# solar_azimuth_series = convert_east_to_north_radians_convention(solar_azimuth_series)
# convert east to north zero degrees convention --------------------------
# if (
# not isfinite(solar_azimuth.degrees)
# or not solar_azimuth.min_degrees <= solar_azimuth.degrees <= solar_azimuth.max_degrees
# ):
# raise ValueError(
# f"The calculated solar azimuth angle {solar_azimuth.degrees} is out of the expected range\
# [{solar_azimuth.min_degrees}, {solar_azimuth.max_degrees}] degrees"
# )
if (
(solar_azimuth_series < SolarAzimuth().min_radians)
| (solar_azimuth_series > SolarAzimuth().max_radians)
).any():
out_of_range_values = solar_azimuth_series[
(solar_azimuth_series < SolarAzimuth().min_radians)
| (solar_azimuth_series > SolarAzimuth().max_radians)
]
# raise ValueError(# ?
logger.warning(
f"{WARNING_NEGATIVE_VALUES} "
f"[{SolarAzimuth().min_radians}, {SolarAzimuth().max_radians}] radians"
f" in [code]solar_azimuth_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_azimuth_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAzimuth(
value=solar_azimuth_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.hofierka,
solar_timing_algorithm=SolarTimeModel.pvgis,
origin=azimuth_origin,
# definition=incidence_angle_definition,
# description=incidence_angle_description,
) # zero_direction='East'
solar_declination ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_declination_series_hofierka | Approximate the sun's declination for a given day of the year. |
calculate_solar_declination_series_hofierka ¶
calculate_solar_declination_series_hofierka(
timestamps: DatetimeIndex,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarDeclination
Approximate the sun's declination for a given day of the year.
The solar declination is the angle between the Sun's rays and the equatorial plane of Earth. It varies throughout the year due to the tilt of the Earth's axis and is an important parameter in determining the seasons and the amount of solar radiation received at different latitudes.
The function calculates the proportion of the way through the year (in radians), which is given by (2 * pi * day_of_year) / 365.25. The 0.3978, 1.4, and 0.0355 are constants in the approximation formula, with the 0.0489 being an adjustment factor for the slight eccentricity of Earth's orbit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
day_of_year | The day of the year (ranging from 1 to 365 or 366 in a leap year). | required |
Returns:
| Name | Type | Description |
|---|---|---|
solar_declination | float | The solar declination in radians for the given day of the year. |
Notes
The equation used here is a simple approximation and bases upon a direct translation from PVGIS' rsun3 source code:
- from file: rsun_base.cpp
- function: com_declin(no_of_day)
For more accurate calculations of solar position, comprehensive models like the Solar Position Algorithm (SPA) are typically used.
Source code in pvgisprototype/algorithms/hofierka/position/solar_declination.py
@log_function_call
@custom_cached
def calculate_solar_declination_series_hofierka(
timestamps: DatetimeIndex,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarDeclination:
"""Approximate the sun's declination for a given day of the year.
The solar declination is the angle between the Sun's rays and the
equatorial plane of Earth. It varies throughout the year due to the tilt of
the Earth's axis and is an important parameter in determining the seasons
and the amount of solar radiation received at different latitudes.
The function calculates the `proportion` of the way through the year (in
radians), which is given by `(2 * pi * day_of_year) / 365.25`.
The `0.3978`, `1.4`, and `0.0355` are constants in the approximation
formula, with the `0.0489` being an adjustment factor for the slight
eccentricity of Earth's orbit.
Parameters
----------
day_of_year: int
The day of the year (ranging from 1 to 365 or 366 in a leap year).
Returns
-------
solar_declination: float
The solar declination in radians for the given day of the year.
Notes
-----
The equation used here is a simple approximation and bases upon a direct
translation from PVGIS' rsun3 source code:
- from file: rsun_base.cpp
- function: com_declin(no_of_day)
For more accurate calculations of solar position, comprehensive models like
the Solar Position Algorithm (SPA) are typically used.
"""
day_angle_series = calculate_day_angle_series_hofierka(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
# Note the - sign for the output solar declination, as is in PVGIS v5.2
# see : com_declin() in rsun_base.c
solar_declination_series = numpy.arcsin(
0.3978
* numpy.sin(
day_angle_series.radians
- 1.4
+ eccentricity_amplitude
* numpy.sin(day_angle_series.radians - eccentricity_phase_offset)
)
)
out_of_range, out_of_range_index = identify_values_out_of_range_x(
series=solar_declination_series,
shape=timestamps.shape,
data_model=SolarDeclination(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_declination_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarDeclination(
value=solar_declination_series,
unit=RADIANS,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
solar_positioning_algorithm=day_angle_series.solar_positioning_algorithm, # ?
)
solar_hour_angle ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_hour_angle_series_hofierka | Calculate the hour angle ω' |
calculate_solar_hour_angle_series_hofierka ¶
calculate_solar_hour_angle_series_hofierka(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarHourAngle
Calculate the hour angle ω'
ω = (ST / 3600 - 12) * 15 * pi / 180
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solar_time | The solar time (ST) is a calculation of the passage of time based on the position of the Sun in the sky. It is expected to be decimal hours in a 24 hour format and measured internally in seconds. | required |
Returns:
| Name | Type | Description |
|---|---|---|
hour_angle | float | The solar hour angle (ω) is the angle at any instant through which the earth has to turn to bring the meridian of the observer directly in line with the sun's rays measured in radian. |
Notes
If not mistaken, in PVGIS' C source code, the conversion function is:
hour_angle = (solar_time / 3600 - 12) * 15 * 0.0175
where the solar time was given in seconds.
Source code in pvgisprototype/algorithms/hofierka/position/solar_hour_angle.py
@log_function_call
@custom_cached
def calculate_solar_hour_angle_series_hofierka(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarHourAngle:
"""Calculate the hour angle ω'
ω = (ST / 3600 - 12) * 15 * pi / 180
Parameters
----------
solar_time: float
The solar time (ST) is a calculation of the passage of time based on the
position of the Sun in the sky. It is expected to be decimal hours in a
24 hour format and measured internally in seconds.
Returns
--------
hour_angle: float
The solar hour angle (ω) is the angle at any instant through which the
earth has to turn to bring the meridian of the observer directly in
line with the sun's rays measured in radian.
Notes
-----
If not mistaken, in PVGIS' C source code, the conversion function is:
hour_angle = (solar_time / 3600 - 12) * 15 * 0.0175
where the solar time was given in seconds.
"""
from pvgisprototype.algorithms.noaa.solar_time import (
calculate_true_solar_time_series_noaa,
)
true_solar_time_series = calculate_true_solar_time_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
solar_hour_angle_series = (true_solar_time_series.minutes - 720.0) * (
numpy.pi / 720.0
)
# if (
# not isfinite(hour_angle.degrees)
# or not hour_angle.min_degrees <= hour_angle.degrees <= hour_angle.max_degrees
# ):
# raise ValueError(
# f"The calculated solar hour angle {hour_angle.degrees} is out of the expected range\
# [{hour_angle.min_degrees}, {hour_angle.max_degrees}] degrees"
# )
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_hour_angle_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarHourAngle(
value=solar_hour_angle_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.hofierka,
solar_timing_algorithm="Hofierka",
)
solar_incidence ¶
Functions:
| Name | Description |
|---|---|
calculate_relative_longitude | Notes |
calculate_solar_incidence_series_hofierka | Calculate the angle of incidence (θ) between the direction of the sun |
calculate_relative_longitude ¶
calculate_relative_longitude(
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> RelativeLongitude
Notes
Hofierka (2002) uses equations presented by Jenčo (1992) :
tangent_relative_longitude =
(
- sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
In PVGIS' C source code, there is an error of one negative sign in either of the expressions! That is so because : cos(pi/2 + x) = -sin(x). As a consequence, the numerator becomes a positive number.
Source code :
/* These calculations depend on slope and aspect. Constant for the day if not tracking */
sin_phi_l = -gridGeom->coslat * cos_u * sin_v + gridGeom->sinlat * sin_u;
latid_l = asin(sin_phi_l);
cos_latid_l = cos(latid_l);
q1 = gridGeom->sinlat * cos_u * sin_v + gridGeom->coslat * sin_u;
tan_lam_l = - cos_u * cos_v / q1;
longit_l = atan (tan_lam_l);
if((aspect<M_PI)&&(longit_l<0.))
{
longit_l += M_PI;
}
else if((aspect>M_PI)&&(longit_l>0.))
{
longit_l -= M_PI;
}
Translation in to Python / pseudocode :
tangent_relative_longitude =
(
- cos(half_pi - surface_tilt) # cos(pi/2 - x) = sin(x)
* cos(half_pi + surface_orientation) # cos(pi/2 + x) = -sin(x) #
) / (
sin(latitude)
* cos(half_pi - surface_tilt)
* sin(half_pi + surface_orientation) # sin(pi/2 + x) = cos(x)
+ cos(latitude)
* sin(half_pi - surface_tilt) # sin(pi/2 - x) = cos(x)
)
As a consequence, PVGIS is like (note the positive numerator!) :
tangent_relative_longitude =
(
sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
Source code in pvgisprototype/algorithms/hofierka/position/solar_incidence.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateRelativeLongitudeInputModel)
def calculate_relative_longitude(
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> RelativeLongitude:
"""
Notes
-----
Hofierka (2002) uses equations presented by Jenčo (1992) :
tangent_relative_longitude =
(
- sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
In PVGIS' C source code, there is an error of one negative sign in either
of the expressions! That is so because : cos(pi/2 + x) = -sin(x).
As a consequence, the numerator becomes a positive number.
Source code :
/* These calculations depend on slope and aspect. Constant for the day if not tracking */
sin_phi_l = -gridGeom->coslat * cos_u * sin_v + gridGeom->sinlat * sin_u;
latid_l = asin(sin_phi_l);
cos_latid_l = cos(latid_l);
q1 = gridGeom->sinlat * cos_u * sin_v + gridGeom->coslat * sin_u;
tan_lam_l = - cos_u * cos_v / q1;
longit_l = atan (tan_lam_l);
if((aspect<M_PI)&&(longit_l<0.))
{
longit_l += M_PI;
}
else if((aspect>M_PI)&&(longit_l>0.))
{
longit_l -= M_PI;
}
Translation in to Python / pseudocode :
tangent_relative_longitude =
(
- cos(half_pi - surface_tilt) # cos(pi/2 - x) = sin(x)
* cos(half_pi + surface_orientation) # cos(pi/2 + x) = -sin(x) #
) / (
sin(latitude)
* cos(half_pi - surface_tilt)
* sin(half_pi + surface_orientation) # sin(pi/2 + x) = cos(x)
+ cos(latitude)
* sin(half_pi - surface_tilt) # sin(pi/2 - x) = cos(x)
)
As a consequence, PVGIS is like (note the positive numerator!) :
tangent_relative_longitude =
(
sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
"""
# -----------------------------------------------------------------------
# in PVGIS an extra minus sign results to an all positive numerator!
# tangent_relative_longitude_numerator = sin(surface_tilt.radians) * sin(
# surface_orientation.radians
# )
# -----------------------------------------------------------------------
tangent_relative_longitude_numerator = -sin(
surface_tilt.radians
) * -sin( # cos(pi/2 - surface_tilt.radians)
surface_orientation.radians
) # cos(pi/2 + surface_orientation.radians)
tangent_relative_longitude_denominator = sin(latitude.radians) * sin(
surface_tilt.radians
) * cos( # cos(pi/2 - surface_tilt.radians)
surface_orientation.radians
) + cos( # sin(pi/2 + surface_orientation.radians)
latitude.radians
) * cos(
surface_tilt.radians
) # sin(pi/2 - surface_tilt.radians)
# force dtype !
relative_longitude = numpy.array(
[
numpy.arctan(
tangent_relative_longitude_numerator
/ tangent_relative_longitude_denominator
)
],
dtype=dtype,
)
if surface_orientation.radians < pi and relative_longitude < 0:
relative_longitude += pi
if surface_orientation.radians > pi and relative_longitude > 0:
relative_longitude -= pi
log_data_fingerprint(
data=relative_longitude,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return RelativeLongitude(
value=relative_longitude,
unit=RADIANS,
)
calculate_solar_incidence_series_hofierka ¶
calculate_solar_incidence_series_hofierka(
longitude: Longitude,
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex = str(now_utc_datetimezone()),
timezone: ZoneInfo | None = None,
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
solar_time_model: SolarTimeModel = milne,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence
Calculate the angle of incidence (θ) between the direction of the sun ray and the line normal to the surface measured in radian.
θ = acos( sin(Φ) * ( sin(δ) * cos(β) + cos(δ) * cos(γ) * cos(ω) * sin(β) ) + cos(Φ) * (cos(δ) * cos(ω) * cos(β) - sin(δ) * cos(γ) * sin(β)) + cos(δ) * sin(γ) * sin(ω) * sin(β) )
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
latitude | Latitude | Latitude is the angle (Φ) between the sun's rays and its projection on the horizontal surface measured in radian. | required |
surface_tilt | SurfaceTilt | Surface tilt or slope is the angle (β) between the inclined slope and the horizontal plane measured in radian. | SURFACE_TILT_DEFAULT |
surface_orientiation | Surface orientation or azimuth is the angle (γ) in the horizontal plane between the line due south and the horizontal projection of the normal to the inclined plane surface measured in radian. | required |
Returns:
| Name | Type | Description |
|---|---|---|
solar_incidence | float | The angle of incidence (θ) is the angle between the direction of the sun ray and the line normal to the surface measured in radian. |
Source code in pvgisprototype/algorithms/hofierka/position/solar_incidence.py
@log_function_call
# @validate_with_pydantic(CalculateSolarIncidenceTimeSeriesPVISInputModel)
def calculate_solar_incidence_series_hofierka(
longitude: Longitude,
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex = str(now_utc_datetimezone()),
timezone: ZoneInfo | None = None,
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
solar_time_model: SolarTimeModel = SolarTimeModel.milne,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence:
"""Calculate the angle of incidence (θ) between the direction of the sun
ray and the line normal to the surface measured in radian.
θ =
acos(
sin(Φ)
* (
sin(δ) * cos(β) + cos(δ) * cos(γ) * cos(ω) * sin(β)
)
+ cos(Φ) * (cos(δ) * cos(ω) * cos(β) - sin(δ) * cos(γ) * sin(β))
+ cos(δ)
* sin(γ)
* sin(ω)
* sin(β)
)
Parameters
----------
latitude: float
Latitude is the angle (Φ) between the sun's rays and its projection on
the horizontal surface measured in radian.
surface_tilt: float
Surface tilt or slope is the angle (β) between the inclined slope and
the horizontal plane measured in radian.
surface_orientiation: float
Surface orientation or azimuth is the angle (γ) in the horizontal plane
between the line due south and the horizontal projection of the normal
to the inclined plane surface measured in radian.
Returns
-------
solar_incidence: float
The angle of incidence (θ) is the angle between the direction of the
sun ray and the line normal to the surface measured in radian.
"""
solar_declination_series = calculate_solar_declination_series_hofierka(
timestamps=timestamps,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
)
solar_hour_angle_series = calculate_solar_hour_angle_series_hofierka(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
)
solar_altitude_series = calculate_solar_altitude_series_hofierka(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
sine_relative_inclined_latitude = -cos(latitude.radians) * sin(
surface_tilt.radians
) * cos( # cos(pi/2 - surface_tilt.radians)
surface_orientation.radians
) + sin( # sin(pi/2 + surface_orientation.radians)
latitude.radians
) * cos(
surface_tilt.radians
) # sin(pi/2 - surface_tilt.radians)
relative_inclined_latitude = asin(sine_relative_inclined_latitude)
c_inclined_31_series = cos(relative_inclined_latitude) * numpy.cos(
solar_declination_series.radians
)
c_inclined_33_series = sin(relative_inclined_latitude) * numpy.sin(
solar_declination_series.radians
)
relative_longitude = calculate_relative_longitude(
latitude=latitude,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
)
sine_solar_incidence_series = (
c_inclined_31_series
* numpy.cos(-solar_hour_angle_series.radians - relative_longitude.radians)
+ c_inclined_33_series
)
solar_incidence_series = numpy.arcsin(sine_solar_incidence_series)
incidence_angle_definition = SolarIncidence().definition_complementary
incidence_angle_description = SolarIncidence().description_complementary
if not complementary_incidence_angle:
logger.debug(
f":information: Converting solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}...",
alt=f":information: [bold][magenta]Converting[/magenta] solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}[/bold]...",
)
solar_incidence_series = (pi / 2) - solar_incidence_series
incidence_angle_definition = SolarIncidence().definition
incidence_angle_description = SolarIncidence().description
# set negative or below horizon angles to 0 !
if zero_negative_solar_incidence_angle:
solar_incidence_series[
(solar_incidence_series < 0) | (solar_altitude_series.value < 0)
] = NO_SOLAR_INCIDENCE
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_incidence_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarIncidence(
value=solar_incidence_series,
unit=RADIANS,
solar_positioning_algorithm=solar_declination_series.solar_positioning_algorithm, #
solar_timing_algorithm=solar_hour_angle_series.solar_timing_algorithm, #
incidence_algorithm=SolarIncidenceModel.hofierka,
definition=incidence_angle_definition,
description=incidence_angle_description,
# azimuth_origin=solar_azimuth_series.origin,
)
power ¶
Functions:
| Name | Description |
|---|---|
calculate_spectral_photovoltaic_power_output | Calculate the photovoltaic power output over a location (latitude), surface |
calculate_spectrally_resolved_global_inclined_irradiance_series | Calculate the photovoltaic power output over a location (latitude), surface |
calculate_spectral_photovoltaic_power_output ¶
calculate_spectral_photovoltaic_power_output(
longitude: float,
latitude: float,
elevation: float,
timestamps: datetime | None = None,
timezone: str | None = None,
spectrally_resolved_global_horizontal_irradiance_series: (
Path | None
) = None,
spectrally_resolved_direct_horizontal_irradiance_series: (
Path | None
) = None,
number_of_junctions: int = 1,
spectral_response_data: Path | None = None,
standard_conditions_response: Path | None = None,
temperature_series: ndarray = array(
average_air_temperature
),
wind_speed_series: ndarray = array(WIND_SPEED_DEFAULT),
mask_and_scale: bool = False,
neighbor_lookup: MethodForInexactMatches | None = None,
tolerance: float | None = TOLERANCE_DEFAULT,
in_memory: bool = False,
surface_tilt: float | None = SURFACE_TILT_DEFAULT,
surface_orientation: (
float | None
) = SURFACE_ORIENTATION_DEFAULT,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
albedo: float | None = ALBEDO_DEFAULT,
apply_angular_loss_factor: bool = True,
solar_position_model: SolarPositionModel = SOLAR_POSITION_ALGORITHM_DEFAULT,
solar_incidence_model: SolarIncidenceModel = jenco,
solar_time_model: SolarTimeModel = SOLAR_TIME_ALGORITHM_DEFAULT,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
time_output_units: str = MINUTES,
angle_units: str = RADIANS,
angle_output_units: str = RADIANS,
system_efficiency: (
float | None
) = SYSTEM_EFFICIENCY_DEFAULT,
power_model: PhotovoltaicModulePerformanceModel = None,
temperature_model: ModuleTemperatureAlgorithm = None,
efficiency: float | None = None,
verbose: int = VERBOSE_LEVEL_DEFAULT,
)
Calculate the photovoltaic power output over a location (latitude), surface and atmospheric conditions, and an arbitrary period of time based on spectrally resolved direct (beam), diffuse, reflected solar irradiation and ambient temperature to account for the varying effects of the solar spectrum. Considering shadowing effects of the local topography are optionally incorporated. The solar geometry relevant parameters (e.g.sunrise and sunset, declination, extraterrestrial irradiance, daylight length) can be optionally saved in a file.
Source code in pvgisprototype/algorithms/hofierka/power.py
def calculate_spectral_photovoltaic_power_output(
longitude: float,
latitude: float,
elevation: float,
timestamps: datetime | None = None,
timezone: str | None = None,
spectrally_resolved_global_horizontal_irradiance_series: Path | None = None, # global_spectral_radiation, # g_rad_spec
spectrally_resolved_direct_horizontal_irradiance_series: Path | None = None, # direct_spectral_radiation, # d_rad_spec,
number_of_junctions: int = 1,
spectral_response_data: Path | None = None,
standard_conditions_response: Path | None = None, #: float = 1, # STCresponse : read from external data
# extraterrestrial_normal_irradiance_series, # spectral_ext,
temperature_series: np.ndarray = np.array(TemperatureSeries().average_air_temperature), # pres_temperature ?
wind_speed_series: np.ndarray = np.array(WIND_SPEED_DEFAULT),
mask_and_scale: bool = False,
neighbor_lookup: MethodForInexactMatches | None = None,
tolerance: float | None = TOLERANCE_DEFAULT,
in_memory: bool = False,
surface_tilt: float | None = SURFACE_TILT_DEFAULT,
surface_orientation: float | None = SURFACE_ORIENTATION_DEFAULT,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
# unrefracted_solar_zenith: UnrefractedSolarZenith | None = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
albedo: float | None = ALBEDO_DEFAULT,
apply_angular_loss_factor: bool = True,
solar_position_model: SolarPositionModel = SOLAR_POSITION_ALGORITHM_DEFAULT,
solar_incidence_model: SolarIncidenceModel = SolarIncidenceModel.jenco,
solar_time_model: SolarTimeModel = SOLAR_TIME_ALGORITHM_DEFAULT,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
# module_temperature,
# horizonpointer,
time_output_units: str = MINUTES,
angle_units: str = RADIANS,
angle_output_units: str = RADIANS,
system_efficiency: float | None = SYSTEM_EFFICIENCY_DEFAULT,
power_model: PhotovoltaicModulePerformanceModel = None,
temperature_model: ModuleTemperatureAlgorithm = None,
efficiency: float | None = None,
verbose: int = VERBOSE_LEVEL_DEFAULT,
):
"""
Calculate the photovoltaic power output over a location (latitude), surface
and atmospheric conditions, and an arbitrary period of time based on
spectrally resolved direct (beam), diffuse, reflected solar irradiation and
ambient temperature to account for the varying effects of the solar
spectrum. Considering shadowing effects of the local topography are
optionally incorporated. The solar geometry relevant parameters
(e.g.sunrise and sunset, declination, extraterrestrial irradiance, daylight
length) can be optionally saved in a file.
"""
spectrally_resolved_global_irradiance_series = calculate_spectrally_resolved_global_inclined_irradiance_series(
longitude=longitude,
latitude=latitude,
elevation=elevation,
timestamps=timestamps,
timezone=timezone,
spectrally_resolved_global_horizontal_irradiance_series=spectrally_resolved_global_horizontal_irradiance_series,
spectrally_resolved_direct_horizontal_irradiance_series=spectrally_resolved_direct_horizontal_irradiance_series,
mask_and_scale=mask_and_scale,
neighbor_lookup=neighbor_lookup,
tolerance=tolerance,
in_memory=in_memory,
surface_tilt=surface_tilt,
surface_orientation=surface_orientation,
linke_turbidity_factor_series=linke_turbidity_factor_series,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
albedo=albedo,
apply_angular_loss_factor=apply_angular_loss_factor,
solar_position_model=solar_position_model,
solar_incidence_model=solar_incidence_model,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
time_output_units=time_output_units,
angle_units=angle_units,
angle_output_units=angle_output_units,
system_efficiency=system_efficiency,
power_model=power_model,
temperature_model=temperature_model,
efficiency=efficiency,
verbose=verbose,
)
# In PVGIS' source code :
# if spectral_band_number < 19:
# global_power_1050 += global_spectral_power[spectral_band_number]
index_1050 = np.max(np.where(BAND_LIMITS < 1050)[0])
global_irradiance_series_up_to_1050 = spectrally_resolved_global_irradiance_series[
:, index_1050
].sum()
bandwidths = np.diff(BAND_LIMITS)
spectral_power_density_up_to_1050 = global_irradiance_series_up_to_1050 / bandwidths
spectral_response_data = read_spectral_response(spectral_response_data)
spectral_response_wavelengths = spectral_response_data[0] # response_wavelengths,
spectral_response = spectral_response_data[1]
standard_conditions_response = spectral_response_data[2] # STCresponse
spectral_factor = calculate_spectral_factor(
global_total_power=spectrally_resolved_global_irradiance_series,
spectral_power_density=spectral_power_density_up_to_1050, # !
number_of_junctions=number_of_junctions,
response_wavelengths=spectral_response_wavelengths,
spectral_response=spectral_response,
standard_conditions_response=standard_conditions_response,
)
spectrally_resolved_photovoltaic_power = (
spectrally_resolved_global_irradiance_series * spectral_factor
)
if efficiency: # user-set
efficiency_coefficient_series = calculate_photovoltaic_efficiency_series(
irradiance_series=spectrally_resolved_global_irradiance_series, # global_total_power,
spectrally_factor=spectral_factor, # internally will do *= global_total_power
temperature_series=temperature_series, # pres_temperature,
model_constants=EFFICIENCY_MODEL_COEFFICIENTS_DEFAULT,
standard_test_temperature=TemperatureSeries().average_air_temperature,
wind_speed_series=wind_speed_series,
power_model=power_model,
temperature_model=temperature_model,
verbose=0, # no verbosity here by choice!
)
spectrally_resolved_photovoltaic_power *= efficiency_coefficient_series
return spectrally_resolved_photovoltaic_power
calculate_spectrally_resolved_global_inclined_irradiance_series ¶
calculate_spectrally_resolved_global_inclined_irradiance_series(
longitude: float,
latitude: float,
elevation: float,
timestamps: datetime | None = None,
timezone: str | None = None,
spectrally_resolved_global_horizontal_irradiance_series: (
Path | None
) = None,
spectrally_resolved_direct_horizontal_irradiance_series: (
Path | None
) = None,
mask_and_scale: bool = False,
neighbor_lookup: MethodForInexactMatches = None,
tolerance: float | None = TOLERANCE_DEFAULT,
in_memory: bool = False,
surface_tilt: float | None = SURFACE_TILT_DEFAULT,
surface_orientation: (
float | None
) = SURFACE_ORIENTATION_DEFAULT,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
albedo: float | None = ALBEDO_DEFAULT,
apply_angular_loss_factor: bool = True,
solar_position_model: SolarPositionModel = SOLAR_POSITION_ALGORITHM_DEFAULT,
solar_incidence_model: SolarIncidenceModel = jenco,
solar_time_model: SolarTimeModel = SOLAR_TIME_ALGORITHM_DEFAULT,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
time_output_units: str = MINUTES,
angle_units: str = RADIANS,
angle_output_units: str = RADIANS,
verbose: int = VERBOSE_LEVEL_DEFAULT,
)
Calculate the photovoltaic power output over a location (latitude), surface and atmospheric conditions, and an arbitrary period of time based on spectrally resolved direct (beam), diffuse, reflected solar irradiation and ambient temperature to account for the varying effects of the solar spectrum. Considering shadowing effects of the local topography are optionally incorporated. The solar geometry relevant parameters (e.g.sunrise and sunset, declination, extraterrestrial irradiance, daylight length) can be optionally saved in a file.
Source code in pvgisprototype/algorithms/hofierka/power.py
def calculate_spectrally_resolved_global_inclined_irradiance_series(
longitude: float,
latitude: float,
elevation: float,
timestamps: datetime | None = None,
timezone: str | None = None,
spectrally_resolved_global_horizontal_irradiance_series: Path | None = None, # global_spectral_radiation, # g_rad_spec
spectrally_resolved_direct_horizontal_irradiance_series: Path | None = None, # direct_spectral_radiation, # d_rad_spec,
mask_and_scale: bool = False,
neighbor_lookup: MethodForInexactMatches = None,
tolerance: float | None = TOLERANCE_DEFAULT,
in_memory: bool = False,
surface_tilt: float | None = SURFACE_TILT_DEFAULT,
surface_orientation: float | None = SURFACE_ORIENTATION_DEFAULT,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
adjust_for_atmospheric_refraction: bool = True,
# unrefracted_solar_zenith: UnrefractedSolarZenith | None = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
albedo: float | None = ALBEDO_DEFAULT,
apply_angular_loss_factor: bool = True,
solar_position_model: SolarPositionModel = SOLAR_POSITION_ALGORITHM_DEFAULT,
solar_incidence_model: SolarIncidenceModel = SolarIncidenceModel.jenco,
solar_time_model: SolarTimeModel = SOLAR_TIME_ALGORITHM_DEFAULT,
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
# module_temperature,
# horizonpointer,
time_output_units: str = MINUTES,
angle_units: str = RADIANS,
angle_output_units: str = RADIANS,
verbose: int = VERBOSE_LEVEL_DEFAULT,
):
"""
Calculate the photovoltaic power output over a location (latitude), surface
and atmospheric conditions, and an arbitrary period of time based on
spectrally resolved direct (beam), diffuse, reflected solar irradiation and
ambient temperature to account for the varying effects of the solar
spectrum. Considering shadowing effects of the local topography are
optionally incorporated. The solar geometry relevant parameters
(e.g.sunrise and sunset, declination, extraterrestrial irradiance, daylight
length) can be optionally saved in a file.
"""
# Initialize arrays ?
spectrally_resolved_direct_irradiance_series = np.zeros_like(
spectrally_resolved_global_horizontal_irradiance_series
)
spectrally_resolved_diffuse_irradiance_series = np.zeros_like(
spectrally_resolved_global_horizontal_irradiance_series
)
spectrally_resolved_reflected_irradiance_series = np.zeros_like(
spectrally_resolved_global_horizontal_irradiance_series
)
# In r.pv_spec : s0 = lumcline2()
solar_incidence_series = model_solar_incidence_series(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
solar_incidence_model=solar_incidence_model,
surface_tilt=surface_tilt,
surface_orientation=surface_orientation,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
time_output_units=time_output_units,
angle_units=angle_units,
verbose=0,
)
solar_altitude_series = model_solar_altitude_series(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
solar_position_model=solar_position_model,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
solar_time_model=solar_time_model,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
time_output_units=time_output_units,
angle_units=angle_units,
verbose=0,
)
sun_above_horizon = solar_altitude_series.value > 0 # or : .radians ?
positive_solar_incidence = solar_incidence_series.radians > 0
surface_in_shade = is_surface_in_shade_series(solar_altitude_series.value)
surface_not_in_shade = ~surface_in_shade
positive_solar_incidence_and_surface_not_in_shade = np.logical_and(
positive_solar_incidence, surface_not_in_shade
)
if not np.any(sun_above_horizon):
spectrally_resolved_global_irradiance_series = 0
else: # if np.any(positive_solar_altitude)
# We don't need a for loop! ==========================================
# for spectral_band_number in range(1, number_of_spectral_bands + 1):
# We don't need a for loop! ==========================================
# The following stems from PVGIS' C/C++ source code ==================
# Is it necessary ?
# Is it possible for the direct component to be greater than the global one ?
# Does it imply issues in the input data ?
spectrally_resolved_global_horizontal_irradiance_series[
spectrally_resolved_global_horizontal_irradiance_series < 0
] = 0
spectrally_resolved_direct_horizontal_irradiance_series = np.minimum(
spectrally_resolved_global_horizontal_irradiance_series,
spectrally_resolved_direct_horizontal_irradiance_series,
)
# Above stems from PVGIS' C/C++ source code =========================
if not np.any(
positive_solar_incidence_and_surface_not_in_shade
): # get the direct irradiance
spectrally_resolved_direct_irradiance_series = 0
spectrally_resolved_direct_horizontal_irradiance_series = 0
else:
# sunRadVar["cbh"] = global_spectral_radiation[spectral_band_number]
# sunRadVar["cdh"] = direct_spectral_radiation[spectral_band_number]
# extraterrestrial_normal_irradiance_series = EXTRATERRESTRIAL_NORMAL_IRRADIANCE[spectral_band_number]
# extraterrestrial_normal_irradiance_series = (
# calculate_extraterrestrial_normal_irradiance_series(
# timestamps=timestamps,
# solar_constant=solar_constant,
# eccentricity_phase_offset=eccentricity_phase_offset,
# eccentricity_amplitude=eccentricity_amplitude,
# )
# )
# following is in r.pv_spec : `ra`
spectrally_resolved_direct_irradiance_series[
positive_solar_incidence_and_surface_not_in_shade
] = (
calculate_direct_inclined_irradiance(
longitude=longitude,
latitude=latitude,
elevation=elevation,
timestamps=timestamps,
timezone=timezone,
direct_horizontal_component=spectrally_resolved_direct_horizontal_irradiance_series,
mask_and_scale=mask_and_scale,
neighbor_lookup=neighbor_lookup,
tolerance=tolerance,
in_memory=in_memory,
surface_tilt=surface_tilt,
surface_orientation=surface_orientation,
linke_turbidity_factor_series=linke_turbidity_factor_series,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
apply_angular_loss_factor=apply_angular_loss_factor,
solar_position_model=solar_position_model,
solar_incidence_model=solar_incidence_model,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
time_output_units=time_output_units,
angle_units=angle_units,
angle_output_units=angle_output_units,
verbose=0, # no verbosity here by choice!
)
)[
positive_solar_incidence_and_surface_not_in_shade
]
# Calculate diffuse and reflected irradiance for sun above horizon
spectrally_resolved_diffuse_irradiance_series[sun_above_horizon] = (
calculate_diffuse_inclined_irradiance(
longitude=longitude,
latitude=latitude,
elevation=elevation,
timestamps=timestamps,
timezone=timezone,
surface_tilt=surface_tilt,
surface_orientation=surface_orientation,
linke_turbidity_factor_series=linke_turbidity_factor_series,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
global_horizontal_component=spectrally_resolved_global_horizontal_irradiance_series,
direct_horizontal_component=spectrally_resolved_direct_horizontal_irradiance_series,
apply_angular_loss_factor=apply_angular_loss_factor,
solar_position_model=solar_position_model,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
time_output_units=time_output_units,
angle_units=angle_units,
angle_output_units=angle_output_units,
neighbor_lookup=neighbor_lookup,
verbose=0, # no verbosity here by choice!
)[sun_above_horizon]
)
spectrally_resolved_reflected_irradiance_series[sun_above_horizon] = (
calculate_ground_reflected_inclined_irradiance_series(
longitude=longitude,
latitude=latitude,
elevation=elevation,
timestamps=timestamps,
timezone=timezone,
surface_tilt=surface_tilt,
surface_orientation=surface_orientation,
linke_turbidity_factor_series=linke_turbidity_factor_series,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
# unrefracted_solar_zenith=unrefracted_solar_zenith,
albedo=albedo,
direct_horizontal_component=spectrally_resolved_direct_horizontal_irradiance_series,
apply_angular_loss_factor=apply_angular_loss_factor,
solar_position_model=solar_position_model,
solar_time_model=solar_time_model,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
time_output_units=time_output_units,
angle_units=angle_units,
verbose=0, # no verbosity here by choice!
)[sun_above_horizon]
)
spectrally_resolved_global_irradiance_series = (
spectrally_resolved_direct_irradiance_series
+ spectrally_resolved_diffuse_irradiance_series
+ spectrally_resolved_reflected_irradiance_series
)
return spectrally_resolved_global_irradiance_series
read ¶
Functions:
| Name | Description |
|---|---|
read_spectral_response | Read spectral response data from a file. |
read_spectral_response ¶
Read spectral response data from a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filename | str | Path to the file containing spectral response data. | required |
Returns:
| Type | Description |
|---|---|
tuple | A tuple containing: - wavelengths: 1D NumPy array of wavelengths. - response: 2D NumPy array of spectral response values. - STCresponse: Float value of Standard Test Condition response. |
Raises:
| Type | Description |
|---|---|
IOError | If the file cannot be opened or read correctly. |
Source code in pvgisprototype/algorithms/hofierka/read.py
def read_spectral_response(
filename: str,
) -> tuple:
"""
Read spectral response data from a file.
Parameters
----------
filename : str
Path to the file containing spectral response data.
Returns
-------
tuple
A tuple containing:
- wavelengths: 1D NumPy array of wavelengths.
- response: 2D NumPy array of spectral response values.
- STCresponse: Float value of Standard Test Condition response.
Raises
------
IOError
If the file cannot be opened or read correctly.
"""
try:
with open(filename, "r") as fp:
STCresponse = float(fp.readline().strip())
data = np.loadtxt(fp, delimiter=",", ndmin=2)
except IOError as e:
raise IOError(
f"Could not open or read spectral response file: {filename}"
) from e
if data.size == 0:
return np.array([]), np.array([]), STCresponse
wavelengths = data[:, 0]
response = data[:, 1:].T # Transposing to rearrange spectral responses
return wavelengths, response, STCresponse
spectrum ¶
Modules:
| Name | Description |
|---|---|
average_photon_energy | |
spectral_factor | |
average_photon_energy ¶
Functions:
| Name | Description |
|---|---|
calculate_average_photon_energy | The Average Photon Energy (APE) characterises the energetic distribution |
calculate_average_photon_energy ¶
calculate_average_photon_energy(
spectrally_resolved_global_irradiance_series,
global_irradiance_series_up_to_1050,
photon_flux_density,
electron_charge=ELECTRON_CHARGE,
)
The Average Photon Energy (APE) characterises the energetic distribution in an irradiance spectrum. It is calculated by dividing the irradiance [W/m² or eV/m²/sec] by the photon flux density [number of photons/m²/sec].
Notes
From [1]_ :
"The average photon energy is a useful parameter for examining spectral
effects on the performance of amorphous silicon cells. It is strongly
correlated with the useful fraction, but is a device independent
parameter that does not require knowledge of the absorption profile of
a given device.
It is possible to examine the purely spectral performance of amorphous
silicon devices after removing temperature effects from the data.
Single junction devices show a linear increase in corrected ISC/Gtotal
as the received radiation becomes more blue shifted, as a greater
proportion of the insolation lies within its absorption window.
Double and triple junction devices do not vary linearly. The devices
investigated here reach maxima at 1.72 and 1.7 eV, respectively. As the
received spectrum becomes either red or blue shifted from this ideal,
performance drops off due to mismatch between the absorption profile
and the received spectrum. The output of multijunction devices is
essentially limited by the layer generating the least current. The
performance of triple junction cells is more susceptible to changes in
the incident spectrum than double junction cells, although this will be
countermanded with lower degradation in the case of a-Si devices.
The maximum spectral performance of multijunction devices occurs at
APEs higher than the APE where most energy is received. There is an
opportunity to improve the spectral performance of multijunction
devices such that they are most efficient at APEs where the majority of
the energy is delivered.
References
.. [1] Jardine, C.N. & Gottschalg, Ralph & Betts, Thomas & Infield, David. (2002). Influence of Spectral Effects on the Performance of Multijunction Amorphous Silicon Cells. to be published.
Source code in pvgisprototype/algorithms/hofierka/spectrum/average_photon_energy.py
def calculate_average_photon_energy( # series ?
spectrally_resolved_global_irradiance_series,
global_irradiance_series_up_to_1050,
photon_flux_density,
electron_charge=ELECTRON_CHARGE,
):
"""
The Average Photon Energy (APE) characterises the energetic distribution
in an irradiance spectrum. It is calculated by dividing the irradiance
[W/m² or eV/m²/sec] by the photon flux density [number of photons/m²/sec].
Notes
-----
From [1]_ :
"The average photon energy is a useful parameter for examining spectral
effects on the performance of amorphous silicon cells. It is strongly
correlated with the useful fraction, but is a device independent
parameter that does not require knowledge of the absorption profile of
a given device.
It is possible to examine the purely spectral performance of amorphous
silicon devices after removing temperature effects from the data.
Single junction devices show a linear increase in corrected ISC/Gtotal
as the received radiation becomes more blue shifted, as a greater
proportion of the insolation lies within its absorption window.
Double and triple junction devices do not vary linearly. The devices
investigated here reach maxima at 1.72 and 1.7 eV, respectively. As the
received spectrum becomes either red or blue shifted from this ideal,
performance drops off due to mismatch between the absorption profile
and the received spectrum. The output of multijunction devices is
essentially limited by the layer generating the least current. The
performance of triple junction cells is more susceptible to changes in
the incident spectrum than double junction cells, although this will be
countermanded with lower degradation in the case of a-Si devices.
The maximum spectral performance of multijunction devices occurs at
APEs higher than the APE where most energy is received. There is an
opportunity to improve the spectral performance of multijunction
devices such that they are most efficient at APEs where the majority of
the energy is delivered.
References
----------
.. [1] Jardine, C.N. & Gottschalg, Ralph & Betts, Thomas & Infield, David.
(2002). Influence of Spectral Effects on the Performance of Multijunction
Amorphous Silicon Cells. to be published.
"""
# name it series ?
# In PVGIS' source code :
# if spectral_band_number < 19:
# number_of_photons += (
# global_spectral_power[spectral_band_number]
# / photon_energies[spectral_band_number]
# )
index_1050 = np.max(np.where(BAND_LIMITS < 1050)[0])
# photon_energies_up_to_1050 = PHOTON_ENERGIES[index_1050]
global_irradiance_series_up_to_1050 = spectrally_resolved_global_irradiance_series[
:, index_1050
].sum()
# ? ----------------------------------------------------------------------
# number_of_photons_up_to_1050 = (
# spectrally_resolved_global_irradiance_series[:, index_1050]
# / photon_energies_up_to_1050
# )
# ------------------------------------------------------------------------
average_photon_energy = (
global_irradiance_series_up_to_1050 / photon_flux_density * electron_charge
)
return average_photon_energy # series
spectral_factor ¶
Functions:
| Name | Description |
|---|---|
calculate_minimum_spectral_mismatch | Returns |
integrate_spectrum_response | |
calculate_minimum_spectral_mismatch ¶
calculate_minimum_spectral_mismatch(
response_wavelengths,
spectral_response,
number_of_junctions: int,
spectral_power_density,
)
Returns:
| Name | Type | Description |
|---|---|---|
minimum_spectral_mismatch | float | |
minimum_junction | By Kirchoff’s Law the overall current produced by the device is only equal to the smallest current produced by an individual junction. This means that the least productive layer in a multi-junction device limits the performance of a multijunction cell [1]_ |
References
.. [1] Jardine, C.N. & Gottschalg, Ralph & Betts, Thomas & Infield, David. (2002). Influence of Spectral Effects on the Performance of Multijunction Amorphous Silicon Cells. to be published.
Source code in pvgisprototype/algorithms/hofierka/spectrum/spectral_factor.py
def calculate_minimum_spectral_mismatch(
response_wavelengths,
spectral_response,
number_of_junctions: int,
spectral_power_density,
):
"""
Returns
-------
minimum_spectral_mismatch: float
minimum_junction:
By Kirchoff’s Law the overall current produced by the device is only
equal to the smallest current produced by an individual junction. This
means that the least productive layer in a multi-junction device limits
the performance of a multijunction cell [1]_
References
----------
.. [1] Jardine, C.N. & Gottschalg, Ralph & Betts, Thomas & Infield, David.
(2002). Influence of Spectral Effects on the Performance of Multijunction
Amorphous Silicon Cells. to be published.
"""
minimum_spectral_mismatch = 0 # FixMe
minimum_junction = 1 # FixMe
for junction in range(number_of_junctions):
spectral_mismatch = integrate_spectrum_response(
spectral_response_frequencies=response_wavelengths,
spectral_response=spectral_response,
kato_limits=junction,
spectral_power_density=spectral_power_density,
)
if spectral_mismatch < minimum_spectral_mismatch:
minimum_spectral_mismatch = spectral_mismatch
minimum_junction = junction
return minimum_spectral_mismatch, minimum_junction
integrate_spectrum_response ¶
integrate_spectrum_response(
spectral_response_frequencies: NDArray[float32] = None,
spectral_response: NDArray[float32] = None,
kato_limits: NDArray[float32] = None,
spectral_power_density: NDArray[float32] = None,
) -> float
Source code in pvgisprototype/algorithms/hofierka/spectrum/spectral_factor.py
def integrate_spectrum_response(
spectral_response_frequencies: NDArray[np.float32] = None,
spectral_response: NDArray[np.float32] = None,
kato_limits: NDArray[np.float32] = None,
spectral_power_density: NDArray[np.float32] = None,
) -> float:
""" """
m = 0
n = 0
# nu_high = float()
# response_low = float()
# response_high = float()
photovoltaic_power = 0
response_low = spectral_response[0]
# nu_low = float()
nu_low = spectral_response_frequencies[0]
number_of_response_values = len(spectral_response_frequencies)
# number_of_kato_limits = len(kato_limits)
while n < number_of_response_values - 1:
if spectral_response_frequencies[n + 1] < kato_limits[m + 1]:
nu_high = spectral_response_frequencies[n + 1]
response_high = spectral_response[n + 1]
else:
nu_high = kato_limits[m + 1]
response_high = spectral_response[n] + (
nu_high - spectral_response_frequencies[n]
) / (
spectral_response_frequencies[n + 1] - spectral_response_frequencies[n]
) * (
spectral_response[n + 1] - spectral_response[n]
)
photovoltaic_power += (
spectral_power_density[m]
* 0.5
* (response_high + response_low)
* (nu_high - nu_low)
)
if spectral_response_frequencies[n + 1] < kato_limits[m + 1]:
n += 1
else:
m += 1
nu_low = nu_high
response_low = response_high
return photovoltaic_power
huld ¶
Modules:
| Name | Description |
|---|---|
efficiency_factor | |
photovoltaic_module | |
efficiency_factor ¶
Functions:
| Name | Description |
|---|---|
calculate_efficiency_factor_series | Calculate the efficiency factor series specific to the photovoltaic |
calculate_efficiency_factor_series ¶
calculate_efficiency_factor_series(
effective_irradiance_series: EffectiveIrradiance,
radiation_cutoff_threshold: float = RADIATION_CUTOFF_THRESHHOLD,
photovoltaic_module: PhotovoltaicModuleModel = CSI_FREE_STANDING,
power_model: PhotovoltaicModulePerformanceModel = king,
temperature_series: TemperatureSeries = TemperatureSeries(
value=average_air_temperature
),
standard_test_temperature: float = standard_test_temperature,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> PhotovoltaicEfficiencyFactor
Calculate the efficiency factor series specific to the photovoltaic module technology.
Calculate the photovoltaic efficiency factor series for a solar irradiance component specific to the photovoltaic module technology, the photovoltaic power rating model and the (adjusted) temperature series. The implemented power rating model [0] is a variant of King's model [1, 2].
References
[0] Thomas Huld, Gabi Friesen, Artur Skoczek, Robert P. Kenny, Tony Sample, Michael Field, Ewan D. Dunlop, A power-rating model for crystalline silicon PV modules, Solar Energy Materials and Solar Cells, Volume 95, Issue 12, 2011, Pages 3359-3369, ISSN 0927-0248, https://doi.org/10.1016/j.solmat.2011.07.026.
[1] D.L. King, J.A. Kratochvil, W.E. Boyson, W.I. Bower, Field experience with a new performance characterization procedure for photovoltaic arrays, in: Proceedings of the second World Conference and Exhibition on Photovoltaic Solar Energy Conversion, Vienna, 1998, pp. 1947–1952.
[2] D.L. King, W.E. Boyson, J.A. Kratochvil, Photovoltaic Array Performance Model, SAND2004-3535, Sandia National Laboratories, 2004.
Source code in pvgisprototype/algorithms/huld/efficiency_factor.py
@log_function_call
def calculate_efficiency_factor_series(
effective_irradiance_series: EffectiveIrradiance, # effective irradiance
radiation_cutoff_threshold: float = RADIATION_CUTOFF_THRESHHOLD,
photovoltaic_module: PhotovoltaicModuleModel = PhotovoltaicModuleModel.CSI_FREE_STANDING,
power_model: PhotovoltaicModulePerformanceModel = PhotovoltaicModulePerformanceModel.king,
temperature_series: TemperatureSeries = TemperatureSeries(
value=TemperatureSeries().average_air_temperature
),
standard_test_temperature: float = TemperatureSeries().standard_test_temperature,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> PhotovoltaicEfficiencyFactor:
"""Calculate the efficiency factor series specific to the photovoltaic
module technology.
Calculate the photovoltaic efficiency factor series for a solar irradiance
component specific to the photovoltaic module technology, the photovoltaic
power rating model and the (adjusted) temperature series. The implemented
power rating model [0] is a variant of King's model [1, 2].
References
----------
[0] Thomas Huld, Gabi Friesen, Artur Skoczek, Robert P. Kenny, Tony Sample,
Michael Field, Ewan D. Dunlop, A power-rating model for crystalline silicon
PV modules, Solar Energy Materials and Solar Cells, Volume 95, Issue 12,
2011, Pages 3359-3369, ISSN 0927-0248,
https://doi.org/10.1016/j.solmat.2011.07.026.
[1] D.L. King, J.A. Kratochvil, W.E. Boyson, W.I. Bower, Field experience
with a new performance characterization procedure for photovoltaic arrays,
in: Proceedings of the second World Conference and Exhibition on
Photovoltaic Solar Energy Conversion, Vienna, 1998, pp. 1947–1952.
[2] D.L. King, W.E. Boyson, J.A. Kratochvil, Photovoltaic Array
Performance Model, SAND2004-3535, Sandia National Laboratories, 2004.
"""
array_parameters = {
"shape": effective_irradiance_series.shape,
"dtype": dtype,
"init_method": "ones",
"backend": array_backend,
}
efficiency_factor_series = create_array(**array_parameters)
# Radiation cutoff handling
relative_irradiance_series = (
0.001 * effective_irradiance_series
) # Conversion to Unit PV Power ?
efficiency_factor_series = where(
relative_irradiance_series <= radiation_cutoff_threshold,
0,
efficiency_factor_series,
)
radiation_cutoff_loss_series = efficiency_factor_series - 1
radiation_cutoff_loss_percentage_series = 100 * radiation_cutoff_loss_series
# low_irradiance_series = ((irradiance_series * efficiency_factor_series) - irradiance_series)
# low_irradiance_percentage_series = 100 * where(
# irradiance_series != 0,
# ((irradiance_series * efficiency_factor_series) - irradiance_series) / irradiance_series,
# 0
# )
photovoltaic_module_efficiency_coefficients = (
get_coefficients_for_photovoltaic_module(photovoltaic_module)
)
# --------------------------------------------------- Is this safe ? -
with np.errstate(divide="ignore", invalid="ignore"):
log_relative_irradiance_series = where(
relative_irradiance_series > 0,
numpy_log(relative_irradiance_series),
0, # -numpy_inf,
)
temperature_deviation_series = temperature_series.value - standard_test_temperature
# power output fom King (using PV eff coeffs for a specific tech) - ... ?
# difference between real conditions and stadard conditions -- should be close to zero
if power_model.value == PhotovoltaicModulePerformanceModel.king:
efficiency_factor_series = (
photovoltaic_module_efficiency_coefficients[0]
+ log_relative_irradiance_series
* (
photovoltaic_module_efficiency_coefficients[1]
+ log_relative_irradiance_series
* photovoltaic_module_efficiency_coefficients[2]
)
+ temperature_deviation_series
* (
photovoltaic_module_efficiency_coefficients[3]
+ log_relative_irradiance_series
* (
photovoltaic_module_efficiency_coefficients[4]
+ log_relative_irradiance_series
* photovoltaic_module_efficiency_coefficients[5]
)
+ photovoltaic_module_efficiency_coefficients[6]
* temperature_deviation_series
)
)
efficiency_factor_series /= photovoltaic_module_efficiency_coefficients[0]
if (
power_model.value == PhotovoltaicModulePerformanceModel.iv
): # 'IV' Model ( Name ? )
current_series = (
CURRENT_AT_STANDARD_TEST_CONDITIONS
+ CURRENT_AT_STANDARD_TEST_CONDITIONS_TEMPERATURE_COEFFICIENT
* temperature_deviation_series
)
voltage_series = (
VOLTAGE_AT_STANDARD_TEST_CONDITIONS
+ VOLTAGE_AT_STANDARD_TEST_CONDITIONS_COEFFICIENT_1
* log_relative_irradiance_series
+ VOLTAGE_AT_STANDARD_TEST_CONDITIONS_COEFFICIENT_2
* log_relative_irradiance_series**2
+ VOLTAGE_AT_STANDARD_TEST_CONDITIONS_TEMPERATURE_COEFFICIENT
* temperature_deviation_series
)
efficiency_factor_series = (
current_series * voltage_series
) / POWER_AT_STANDARD_TEST_CONDITIONS
# # Mask where efficiency is out of range [0, 1]
# mask_invalid_efficiency = (efficiency_series < 0) | (efficiency_series > 1)
# if np.any(mask_invalid_efficiency):
# return "Some calculated efficiencies are out of the expected range [0, 1]!"
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=efficiency_factor_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return PhotovoltaicEfficiencyFactor(
value=efficiency_factor_series,
effective_irradiance=effective_irradiance_series,
log_relative_irradiance=log_relative_irradiance_series,
relative_irradiance=relative_irradiance_series,
low_irradiance=relative_irradiance_series <= radiation_cutoff_threshold,
photovoltaic_module=photovoltaic_module,
photovoltaic_module_efficiency_coefficients=photovoltaic_module_efficiency_coefficients,
power_model=power_model,
radiation_cutoff_threshold=radiation_cutoff_threshold,
radiation_cutoff_loss_percentage=radiation_cutoff_loss_percentage_series,
temperature_deviation=temperature_deviation_series,
)
photovoltaic_module ¶
Functions:
| Name | Description |
|---|---|
get_coefficients_for_photovoltaic_module | Retrieve model coefficients based on the selected PhotovoltaicModuleModel Enum. |
get_coefficients_for_photovoltaic_module ¶
get_coefficients_for_photovoltaic_module(
photovoltaic_module: PhotovoltaicModuleModel,
) -> List[float]
Retrieve model coefficients based on the selected PhotovoltaicModuleModel Enum.
Source code in pvgisprototype/algorithms/huld/photovoltaic_module.py
def get_coefficients_for_photovoltaic_module(
photovoltaic_module: PhotovoltaicModuleModel,
) -> List[float]:
"""Retrieve model coefficients based on the selected PhotovoltaicModuleModel Enum."""
coefficients = PHOTOVOLTAIC_MODULE_COEFFICIENTS_MAP.get(photovoltaic_module, [])
if len(coefficients) < 7: # should be at least
raise ValueError("Insufficient number of model constants!")
return coefficients
iqbal ¶
Modules:
| Name | Description |
|---|---|
solar_incidence | Calculate the solar incidence angle for a surface that is tilted to any |
solar_incidence ¶
Calculate the solar incidence angle for a surface that is tilted to any horizontal and vertical angle, as described by Iqbal [0].
[0] Iqbal, M. “An Introduction to Solar Radiation”. New York: 1983; pp. 23-25.
Functions:
| Name | Description |
|---|---|
calculate_solar_incidence_series_iqbal | Calculate the solar incidence angle for a surface oriented in any |
calculate_solar_incidence_series_iqbal ¶
calculate_solar_incidence_series_iqbal(
longitude: Longitude,
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex = now_utc_datetimezone(),
timezone: ZoneInfo | None = None,
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
sun_horizon_position: List[
SunHorizonPositionModel
] = SUN_HORIZON_POSITION_DEFAULT,
surface_in_shade_series: NpNDArray | None = None,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarIncidence
Calculate the solar incidence angle for a surface oriented in any direction.
Calculate the solar incidence angle between the sun position unit vector and the surface normal unit vector for a surface oriented in any direction; in other words, the cosine of the angle of incidence. Optionally, the output may be the complementary incidence angle between the sun-to-surface vector and the surface plane.
Notes
The equation for the incidence angle I by Iqbal (1983) [0]_ [1]_
I = Arc cos( cos(θ) * cos(ω) + sin(ω) * sin(θ) * (Γ - γ) )
where :
- θ is the solar zenith angle
- ω is the surface tilt angle
- Γ is the astronomers topocentric azimuth angle
- γ is the navigators topocentric azimuth angle
Important observations are :
-
The topocentric astronomers azimuth angle
Γwhich is measured westward from south. -
The surface orientation angle
γ(also referred to as surface azimuth rotation angle) is measured from south to the projection of the surface normal on the horizontal plane, positive or negative if oriented west or east from south, respectively. -
The topocentric azimuth angle
Φfor navigators and solar radiation users (equation 46, p. 588) is measured eastward from north and thus equals the astronomers one plus π or else 180 degrees :
Φ = Γ + 180
and thus
Γ = Φ - 180 [*]
In equation I, the surface orientation angle γ measured from south, is subtracted from the astronomers topocentric azimuth angle which is likewise measured from south. Given that most applications measure azimuthal angles from North, care must be taken to feed the correct "version" of these angles in this function.
PVGIS measures the user-requested azimuthal angles Solar Azimuth (follownig denoted also with Φ) and the "solar radiation"-relevant Surface Orientation from North (follownig denoted as γΝ). Equation I based on [*] becomes relevant for PVGIS in the following form :
I = Arc cos( cos(θ) * cos(ω) + sin(ω) * sin(θ) * (Φ - 180 - γN - 180) )
or else
cosine_solar_incidence_series = (
numpy.cos(solar_zenith_series.radians)
* cos(surface_tilt.radians)
+ sin(surface_tilt.radians)
* numpy.sin(solar_zenith_series.radians)
* numpy.cos(solar_azimuth_series.radians -
surface_orientation.radians - 2 * pi)
)
Nonetheless, and for the sake of consistency with the author's original definition, such conversion are preferrable to be performed in advance and outside the scope of the current function, for both the solar azimuth and the surface orientation angles. Therefore the internal form of the equation uses as per its definition the astronomers topocentric solar azimuth angle and the surface azimuth rotation angle measured from south -- see also source code of this function.
References
.. [0] Iqbal, 1983
.. [1] Equation 47, p. 588),
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | Longitude | | required |
latitude | Latitude | | required |
timestamps | DatetimeIndex | | now_utc_datetimezone() |
timezone | ZoneInfo | | None |
surface_orientation | SurfaceOrientation | Panel azimuth from north. | SURFACE_ORIENTATION_DEFAULT |
surface_tilt | SurfaceTilt | Panel tilt from horizontal. | SURFACE_TILT_DEFAULT |
adjust_for_atmospheric_refraction | bool | | ATMOSPHERIC_REFRACTION_FLAG_DEFAULT |
complementary_incidence_angle | bool | | COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT |
zero_negative_solar_incidence_angle | bool | | ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT |
dtype | str | | DATA_TYPE_DEFAULT |
array_backend | str | | ARRAY_BACKEND_DEFAULT |
verbose | int | | VERBOSE_LEVEL_DEFAULT |
log | int | | LOG_LEVEL_DEFAULT |
Returns:
| Name | Type | Description |
|---|---|---|
solar_incidence_series | SolarIncidence | A times series of solar incidence angles between the sun position vector and the surface normal (or plane) |
Notes
Notes from the original pvlib function :
-
Usage note: When the sun is behind the surface the value returned is negative. For many uses negative values must be set to zero.
-
Input all angles in degrees.
References
.. [0] Iqbal, M. “An Introduction to Solar Radiation”. New York: 1983; pp. 23-25.
Source code in pvgisprototype/algorithms/iqbal/solar_incidence.py
@log_function_call
@custom_cached
def calculate_solar_incidence_series_iqbal(
longitude: Longitude,
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex = now_utc_datetimezone(),
timezone: ZoneInfo | None = None,
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
surface_in_shade_series: NpNDArray | None = None,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarIncidence:
"""Calculate the solar incidence angle for a surface oriented in any
direction.
Calculate the solar incidence angle between the sun position unit vector
and the surface normal unit vector for a surface oriented in any direction;
in other words, the cosine of the angle of incidence. Optionally, the
output may be the complementary incidence angle between the sun-to-surface
vector and the surface plane.
Notes
-----
The equation for the incidence angle `I` by Iqbal (1983) [0]_ [1]_
I = Arc cos( cos(θ) * cos(ω) + sin(ω) * sin(θ) * (Γ - γ) )
where :
- θ is the solar zenith angle
- ω is the surface tilt angle
- Γ is the astronomers topocentric azimuth angle
- γ is the navigators topocentric azimuth angle
Important observations are :
- The topocentric astronomers azimuth angle `Γ` which is measured _westward
from south_.
- The surface orientation angle `γ` (also referred to as surface azimuth
rotation angle) is measured from south to the projection of the surface
normal on the horizontal plane, positive or negative if oriented west or
east from south, respectively.
- The topocentric azimuth angle `Φ` for navigators and solar radiation
users (equation 46, p. 588) is measured _eastward from north_ and thus
equals the astronomers one plus π or else 180 degrees :
Φ = Γ + 180
and thus
Γ = Φ - 180 [*]
In equation I, the surface orientation angle `γ` measured from south, is
subtracted from the astronomers topocentric azimuth angle which is likewise
measured from south. Given that most applications measure azimuthal angles
from North, care must be taken to feed the correct "version" of these
angles in this function.
PVGIS measures the user-requested azimuthal angles Solar Azimuth (follownig
denoted also with Φ) and the "solar radiation"-relevant Surface Orientation
from North (follownig denoted as `γΝ`). Equation I based on [*] becomes
relevant for PVGIS in the following form :
I = Arc cos( cos(θ) * cos(ω) + sin(ω) * sin(θ) * (Φ - 180 - γN - 180) )
or else
cosine_solar_incidence_series = (
numpy.cos(solar_zenith_series.radians)
* cos(surface_tilt.radians)
+ sin(surface_tilt.radians)
* numpy.sin(solar_zenith_series.radians)
* numpy.cos(solar_azimuth_series.radians -
surface_orientation.radians - 2 * pi)
)
Nonetheless, and for the sake of consistency with the author's original
definition, such conversion are preferrable to be performed in advance and
outside the scope of the current function, for both the solar azimuth and
the surface orientation angles. Therefore the internal form of the equation
uses as per its definition the astronomers topocentric solar azimuth angle
and the surface azimuth rotation angle measured from south -- see also
source code of this function.
References
----------
.. [0] Iqbal, 1983
.. [1] Equation 47, p. 588),
Parameters
----------
longitude : Longitude
latitude : Latitude
timestamps : DatetimeIndex
timezone : ZoneInfo
surface_orientation : SurfaceOrientation
Panel azimuth from north.
surface_tilt : SurfaceTilt
Panel tilt from horizontal.
adjust_for_atmospheric_refraction : bool
complementary_incidence_angle : bool
zero_negative_solar_incidence_angle : bool
dtype : str
array_backend : str
verbose : int
log : int
Returns
-------
solar_incidence_series : SolarIncidence
A times series of solar incidence angles between the sun position
vector and the surface normal (or plane)
Notes
-----
Notes from the original pvlib function :
- Usage note: When the sun is behind the surface the value returned is
negative. For many uses negative values must be set to zero.
- Input all angles in degrees.
References
----------
.. [0] Iqbal, M. “An Introduction to Solar Radiation”. New York: 1983; pp. 23-25.
"""
solar_zenith_series = calculate_solar_zenith_series_noaa(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
solar_azimuth_series_north_based = calculate_solar_azimuth_series_noaa(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
dtype=dtype,
array_backend=array_backend,
verbose=0,
log=log,
validate_output=validate_output,
) # North = 0 according to NOAA's solar geometry equations
# array_parameters = {
# "shape": timestamps.shape,
# "dtype": dtype,
# "init_method": "empty",
# "backend": array_backend,
# } # Borrow shape from timestamps
# solar_incidence_series = create_array(**array_parameters)
# Convert to south-based
solar_azimuth_series = SolarAzimuth(
value=(solar_azimuth_series_north_based.radians - pi),
unit=RADIANS,
)
# Φimit Φ to the range from 0° to 360°.
# Divide Φ by 360, record the decimal fraction of the division as F.
# Divide phi by 360 and get the remainder and the fractional part
fraction_series, _ = numpy.modf(solar_azimuth_series.radians / (2 * pi))
# If Φ is positive, then the limited Φ = 360 * F .
# If Φ is negative, then the limited Φ = 360 - 360 *F.
# Remember all of the metadata ! Review-Me & Abstract-Me !
solar_azimuth_series = SolarAzimuth(
value=numpy.where(
solar_azimuth_series.radians >= 0,
2 * pi * fraction_series,
2 * pi - (2 * pi * numpy.abs(fraction_series)),
),
unit=RADIANS,
solar_positioning_algorithm=solar_azimuth_series_north_based.solar_positioning_algorithm,
solar_timing_algorithm=solar_azimuth_series_north_based.solar_timing_algorithm,
origin=solar_azimuth_series_north_based.origin,
)
# named 'projection' in pvlib
cosine_solar_incidence_series = numpy.cos(solar_zenith_series.radians) * cos(
surface_tilt.radians
) + sin(surface_tilt.radians) * numpy.sin(solar_zenith_series.radians) * numpy.cos(
solar_azimuth_series.radians - surface_orientation.radians
) # where :
# solar_azimuth_series : is the astronomers topocentric solar azimuth measured from south
# surface_orientation : is the surface rotation azimuth angle measured from south
# GH 1185 : This is a note from pvlib ?
# projection = numpy.clip(projection, -1, 1)
cosine_solar_incidence_series = numpy.clip(cosine_solar_incidence_series, -1, 1)
solar_incidence_series = numpy.arccos(cosine_solar_incidence_series)
incidence_angle_definition = SolarIncidence().definition_typical # This is the "standard"
incidence_angle_description = SolarIncidence().description_typical
if complementary_incidence_angle:
logger.debug(
f":information: Converting solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}...",
alt=f":information: [bold][magenta]Converting[/magenta] solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}[/bold]...",
)
solar_incidence_series = (pi / 2) - solar_incidence_series
incidence_angle_definition = SolarIncidence().definition_complementary
incidence_angle_description = SolarIncidence().description_complementary
# set negative or below horizon angles ( == solar zenith > 90 ) to 0 !
# Select which solar positions related to the horizon to process
sun_horizon_positions = select_models(
SunHorizonPositionModel, sun_horizon_position
) # Using a callback fails!
# and keep track of the position of the sun relative to the horizon
sun_horizon_position_series = create_array(
timestamps.shape, dtype="object", init_method="empty", backend=array_backend
)
mask_below_horizon = create_array(
timestamps.shape, dtype="bool", init_method="empty", backend=array_backend
)
mask_low_angle = create_array(
timestamps.shape, dtype="bool", init_method="empty", backend=array_backend
)
mask_above_horizon = create_array(
timestamps.shape, dtype="bool", init_method="empty", backend=array_backend
)
# For sun below the horizon
if SunHorizonPositionModel.below in sun_horizon_positions:
mask_below_horizon = solar_zenith_series.value > pi / 2
sun_horizon_position_series[mask_below_horizon] = [
SunHorizonPositionModel.below.value
]
# For very low sun angles
if SunHorizonPositionModel.low_angle in sun_horizon_positions:
mask_low_angle = (
(solar_zenith_series.value <= pi / 2)
& (
solar_zenith_series.value > solar_zenith_series.low_angle_threshold_radians
)
& (sun_horizon_position_series == None) # Operate only on unset elements
)
sun_horizon_position_series[mask_low_angle] = (
SunHorizonPositionModel.low_angle.value
)
if SunHorizonPositionModel.above in sun_horizon_positions:
mask_above_horizon = numpy.logical_and(
(solar_zenith_series.value < pi / 2),
sun_horizon_position_series == None, # operate only on unset elements
)
sun_horizon_position_series[mask_above_horizon] = [
SunHorizonPositionModel.above.value
]
# Combine relevant conditions for no solar incidence
mask_no_solar_incidence_series = numpy.logical_or(
(solar_incidence_series < 0)
| mask_below_horizon
| surface_in_shade_series.value,
sun_horizon_position_series == None,
)
# Zero out negative solar incidence angles : is the default behavior !
if zero_negative_solar_incidence_angle:
logger.debug(
f":information: Setting negative solar incidence angle values to zero...",
alt=f":information: [bold][magenta]Setting[/magenta] [red]negative[/red] solar incidence angle values to [bold]zero[/bold]...",
)
solar_incidence_series = numpy.where(
mask_no_solar_incidence_series,
# (solar_incidence_series < 0) | (solar_altitude_series.value < 0),
NO_SOLAR_INCIDENCE,
solar_incidence_series,
)
log_data_fingerprint(
data=solar_incidence_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
return SolarIncidence(
value=solar_incidence_series,
sun_horizon_position=sun_horizon_position_series,
solar_positioning_algorithm=solar_zenith_series.solar_positioning_algorithm,
solar_timing_algorithm=solar_zenith_series.solar_timing_algorithm,
algorithm=SolarIncidenceModel.iqbal,
definition=incidence_angle_definition, # either the 'typical' or the 'complementary'
description=incidence_angle_description, # same as above
azimuth_origin=solar_azimuth_series.origin,
)
jenco ¶
Modules:
| Name | Description |
|---|---|
solar_altitude | |
solar_azimuth | |
solar_declination | |
solar_incidence | |
solar_altitude ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_altitude_series_jenco | Calculate the solar altitude angle (θ) for a time series at a specific |
calculate_solar_altitude_series_jenco ¶
calculate_solar_altitude_series_jenco(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo | None,
eccentricity_phase_offset: float,
eccentricity_amplitude: float,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
) -> SolarAltitude
Calculate the solar altitude angle (θ) for a time series at a specific geographic latitude and longitude.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | Longitude of the location in radians. | required |
latitude | float | Latitude of the location in radians. | required |
timestamps | DatetimeIndex | Times for which the solar azimuth will be calculated. | required |
timezone | ZoneInfo | Timezone of the location. | required |
dtype | str | Data type for the calculations. | DATA_TYPE_DEFAULT |
array_backend | str | Backend array library to use. | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level of the function. | 0 |
log | int | Log level for the function. | 0 |
Returns:
| Type | Description |
|---|---|
SolarAltitude | A custom data class that hold a NumPy NDArray of calculated solar azimuth angles in radians, a method to convert the angles to degrees and other metadata. |
Notes
References
Examples:
Source code in pvgisprototype/algorithms/jenco/solar_altitude.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarAltitudeTimeSeriesJencoInput)
def calculate_solar_altitude_series_jenco(
longitude: Longitude, # radians
latitude: Latitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo | None,
eccentricity_phase_offset: float,
eccentricity_amplitude: float,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
) -> SolarAltitude:
"""Calculate the solar altitude angle (θ) for a time series at a specific
geographic latitude and longitude.
Parameters
----------
longitude : float
Longitude of the location in radians.
latitude : float
Latitude of the location in radians.
timestamps : DatetimeIndex
Times for which the solar azimuth will be calculated.
timezone : ZoneInfo
Timezone of the location.
dtype : str, optional
Data type for the calculations.
array_backend : str, optional
Backend array library to use.
verbose : int, optional
Verbosity level of the function.
log : int, optional
Log level for the function.
Returns
-------
SolarAltitude
A custom data class that hold a NumPy NDArray of calculated solar
azimuth angles in radians, a method to convert the angles to degrees
and other metadata.
Notes
-----
References
----------
Examples
--------
"""
solar_declination_series = calculate_solar_declination_series_hofierka(
timestamps=timestamps,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
solar_hour_angle_series = calculate_solar_hour_angle_series_hofierka(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
C31 = cos(latitude.radians) * numpy.cos(solar_declination_series.radians)
C33 = sin(latitude.radians) * numpy.sin(solar_declination_series.radians)
sine_solar_altitude_series = C31 * numpy.cos(solar_hour_angle_series.radians) + C33
solar_altitude_series = numpy.arcsin(sine_solar_altitude_series)
# mask_positive_C31 = C31 > 1e-7
# solar_altitude_series[mask_positive_C31] = numpy.where(
# sine_solar_altitude_series < 0,
# NO_SOLAR_INCIDENCE,
# solar_altitude_series,
# )
if (
(solar_altitude_series < SolarAltitude().min_radians)
| (solar_altitude_series > SolarAltitude().max_radians)
).any():
out_of_range_values = solar_altitude_series[
(solar_altitude_series < SolarAltitude().min_radians)
| (solar_altitude_series > SolarAltitude().max_radians)
]
# raise ValueError(# ?
logger.warning(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarAltitude().min_radians}, {SolarAltitude().max_radians}] radians"
f" in [code]solar_altitude_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_altitude_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAltitude(
value=solar_altitude_series,
unit=RADIANS,
solar_positioning_algorithm=solar_declination_series.solar_positioning_algorithm, #
solar_timing_algorithm=solar_hour_angle_series.solar_timing_algorithm, #
)
solar_azimuth ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_azimuth_series_jenco | Calculate the solar azimuth angle (θ) between the sun and meridian |
calculate_solar_azimuth_series_jenco ¶
calculate_solar_azimuth_series_jenco(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
adjust_for_atmospheric_refraction: bool = True,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
) -> SolarAzimuth
Calculate the solar azimuth angle (θ) between the sun and meridian measured from East for a time series at a specific geographic latitude and longitude.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | Longitude of the location in radians. | required |
latitude | float | Latitude of the location in radians. | required |
timestamps | DatetimeIndex | Times for which the solar azimuth will be calculated. | required |
timezone | ZoneInfo | Timezone of the location. | required |
adjust_for_atmospheric_refraction | bool | Whether to correct the solar zenith angle for atmospheric refraction. | True |
dtype | str | Data type for the calculations. | DATA_TYPE_DEFAULT |
array_backend | str | Backend array library to use. | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level of the function. | 0 |
log | int | Log level for the function. | 0 |
Returns:
| Type | Description |
|---|---|
SolarAzimuth | A custom data class that hold a NumPy NDArray of calculated solar azimuth angles in radians, a method to convert the angles to degrees and other metadata. |
Notes
Two important notes on the calculation of the solar azimuth angle :
-
The equation implemented here follows upon the relevant Wikipedia article for the "Solar azimuth angle" [1]_.
-
The angle derived fom the arccosine function requires an adjustment to correctly represent both morning and afternoon solar azimuth angles.
Adjusting the solar azimuth based on the time of day
Given that arccosine ranges in [0, π] or else in [0°, 180°], the raw calculated solar azimuth angle will likewise range in [0, π]. This necessitates an adjustment based on the time of day.
-
Morning (solar hour angle < 0): the azimuth angle is correctly derived directly from the arccosine function representing angles from the North clockwise to the South [0°, 180°].
-
Afternoon (solar hour angle > 0): the azimuth angle needs to be adjusted in order to correctly represent angles going further from the South to the West [180°, 360°]. This is achieved by subtracting the azimuth from 360°.
On the use of arctan2 from Wikipedia :
Corollary: if (y1, x1) and (y2, x2) are 2-dimensional vectors, the difference formula is frequently used in practice to compute the angle between those vectors with the help of atan2, since the resulting computation behaves benign in the range (−π, π] and can thus be used without range checks in many practical situations.
The atan2 function was originally designed for the convention in pure mathematics that can be termed east-counterclockwise. In practical applications, however, the north-clockwise and south-clockwise conventions are often the norm. The solar azimuth angle for example, that uses both the north-clockwise and south-clockwise conventions widely, can be calculated similarly with the east- and north-components of the solar vector as its arguments. Different conventions can be realized by swapping the positions and changing the signs of the x- and y-arguments as follows:
- atan2(y,x) : East-Counterclockwise Convention
- atan2(x,y) : North-Clockwise Convention
- atan2(-x,-y) : South-Clockwise Convention
Changing the sign of the x- and/or y-arguments and/or swapping their positions can create 8 possible variations of the atan2 function and they, interestingly, correspond to 8 possible definitions of the angle, namely, clockwise or counterclockwise starting from each of the 4 cardinal directions, north, east, south and west.
References
.. [0] https://gml.noaa.gov/grad/solcalc/solareqns.PDF
.. [1] https://en.wikipedia.org/wiki/Solar_azimuth_angle#Conventional_Trigonometric_Formulas
.. [2] https://github.com/pvlib/pvlib-python
Examples:
>>> from math import radians
>>> from pvgisprototype.api.utilities.timestamp import generate_datetime_series
>>> timestamps = generate_datetime_series(start_time='2010-01-27', end_time='2010-01-28')
>>> from zoneinfo import ZoneInfo
>>> from pvgisprototype.api.position.azimuth_series import calculate_solar_azimuth_series_noaa
>>> solar_azimuth_series = calculate_solar_azimuth_series_noaa(
... longitude=radians(8.628),
... latitude=radians(45.812),
... timestamps=timestamps,
... timezone=ZoneInfo("UTC"),
... adjust_for_atmospheric_refraction=True
... )
>>> print(solar_azimuth_series)
>>> print(solar_azimuth_series.degrees)
Source code in pvgisprototype/algorithms/jenco/solar_azimuth.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarAzimuthTimeSeriesJencoInput)
def calculate_solar_azimuth_series_jenco(
longitude: Longitude, # radians
latitude: Latitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo,
adjust_for_atmospheric_refraction: bool = True,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
) -> SolarAzimuth:
"""Calculate the solar azimuth angle (θ) between the sun and meridian
measured from East for a time series at a specific geographic latitude
and longitude.
Parameters
----------
longitude : float
Longitude of the location in radians.
latitude : float
Latitude of the location in radians.
timestamps : DatetimeIndex
Times for which the solar azimuth will be calculated.
timezone : ZoneInfo
Timezone of the location.
adjust_for_atmospheric_refraction : bool, optional
Whether to correct the solar zenith angle for atmospheric refraction.
dtype : str, optional
Data type for the calculations.
array_backend : str, optional
Backend array library to use.
verbose : int, optional
Verbosity level of the function.
log : int, optional
Log level for the function.
Returns
-------
SolarAzimuth
A custom data class that hold a NumPy NDArray of calculated solar
azimuth angles in radians, a method to convert the angles to degrees
and other metadata.
Notes
-----
Two important notes on the calculation of the solar azimuth angle :
- The equation implemented here follows upon the relevant Wikipedia article
for the "Solar azimuth angle" [1]_.
- The angle derived fom the arccosine function requires an adjustment to
correctly represent both morning and afternoon solar azimuth angles.
Adjusting the solar azimuth based on the time of day
Given that arccosine ranges in [0, π] or else in [0°, 180°], the raw
calculated solar azimuth angle will likewise range in [0, π]. This
necessitates an adjustment based on the time of day.
- Morning (solar hour angle < 0): the azimuth angle is correctly
derived directly from the arccosine function representing angles from
the North clockwise to the South [0°, 180°].
- Afternoon (solar hour angle > 0): the azimuth angle needs to be
adjusted in order to correctly represent angles going further from
the South to the West [180°, 360°]. This is achieved by subtracting
the azimuth from 360°.
On the use of arctan2 from Wikipedia :
Corollary: if (y1, x1) and (y2, x2) are 2-dimensional vectors, the
difference formula is frequently used in practice to compute the angle
between those vectors with the help of atan2, since the resulting
computation behaves benign in the range (−π, π] and can thus be used
without range checks in many practical situations.
The `atan2` function was originally designed for the convention in pure
mathematics that can be termed east-counterclockwise. In practical
applications, however, the north-clockwise and south-clockwise conventions
are often the norm. The solar azimuth angle for example, that uses both the
north-clockwise and south-clockwise conventions widely, can be calculated
similarly with the east- and north-components of the solar vector as its
arguments. Different conventions can be realized by swapping the positions
and changing the signs of the x- and y-arguments as follows:
- atan2(y,x) : East-Counterclockwise Convention
- atan2(x,y) : North-Clockwise Convention
- atan2(-x,-y) : South-Clockwise Convention
Changing the sign of the x- and/or y-arguments and/or swapping their
positions can create 8 possible variations of the atan2 function and they,
interestingly, correspond to 8 possible definitions of the angle, namely,
clockwise or counterclockwise starting from each of the 4 cardinal
directions, north, east, south and west.
References
----------
.. [0] https://gml.noaa.gov/grad/solcalc/solareqns.PDF
.. [1] https://en.wikipedia.org/wiki/Solar_azimuth_angle#Conventional_Trigonometric_Formulas
.. [2] https://github.com/pvlib/pvlib-python
.. [3] https://github.com/skyfielders/python-skyfield/
.. [4] https://github.com/kylebarron/suncalc-py
Examples
--------
>>> from math import radians
>>> from pvgisprototype.api.utilities.timestamp import generate_datetime_series
>>> timestamps = generate_datetime_series(start_time='2010-01-27', end_time='2010-01-28')
>>> from zoneinfo import ZoneInfo
>>> from pvgisprototype.api.position.azimuth_series import calculate_solar_azimuth_series_noaa
>>> solar_azimuth_series = calculate_solar_azimuth_series_noaa(
... longitude=radians(8.628),
... latitude=radians(45.812),
... timestamps=timestamps,
... timezone=ZoneInfo("UTC"),
... adjust_for_atmospheric_refraction=True
... )
>>> print(solar_azimuth_series)
>>> print(solar_azimuth_series.degrees)
"""
solar_declination_series = calculate_solar_declination_series_jenco(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
solar_hour_angle_series = calculate_solar_hour_angle_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
C11 = sin(latitude.radians) * numpy.cos(solar_declination_series.radians)
C13 = cos(latitude.radians) * numpy.sin(solar_declination_series.radians)
C22 = numpy.cos(solar_declination_series.radians)
x_solar_vector_component = C22 * numpy.sin(solar_hour_angle_series.radians)
y_solar_vector_component = C11 * numpy.cos(solar_hour_angle_series.radians) - C13
# `x` to `y` derives North-Clockwise azimuth
azimuth_origin = "North"
solar_azimuth_series = numpy.mod(
(pi + numpy.arctan2(x_solar_vector_component, y_solar_vector_component)), 2 * pi
)
if (
(solar_azimuth_series < SolarAzimuth().min_radians)
| (solar_azimuth_series > SolarAzimuth().max_radians)
).any():
out_of_range_values = solar_azimuth_series[
(solar_azimuth_series < SolarAzimuth().min_radians)
| (solar_azimuth_series > SolarAzimuth().max_radians)
]
# raise ValueError(# ?
logger.warning(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarAzimuth().min_radians}, {SolarAzimuth().max_radians}] radians"
f" in [code]solar_azimuth_series[/code] : {out_of_range_values}"
)
log_data_fingerprint(
data=solar_azimuth_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAzimuth(
value=solar_azimuth_series,
unit=RADIANS,
# positioning_algorithm=solar_declination_series.solar_positioning_algorithm, #
solar_timing_algorithm=solar_hour_angle_series.solar_timing_algorithm, #
origin=azimuth_origin,
)
solar_declination ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_declination_series_jenco | Approximate the sun's declination for a time series. |
calculate_solar_declination_series_jenco ¶
calculate_solar_declination_series_jenco(
timestamps: DatetimeIndex,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarDeclination
Approximate the sun's declination for a time series.
δ = arcsin (0.3978 sin (j’ - 1.4 + 0.0355 sin (j’ - 0.0489)))
The solar declination is the angle between the Sun's rays and the equatorial plane of Earth. It varies throughout the year due to the tilt of the Earth's axis and is an important parameter in determining the seasons and the amount of solar radiation received at different latitudes.
The function calculates the proportion of the way through the year (in radians), which is given by (2 * pi * day_of_year) / 365.25. The 0.3978, 1.4, and 0.0355 are constants in the approximation formula, with the 0.0489 being an adjustment factor for the slight eccentricity of Earth's orbit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
day_of_year | The day of the year (ranging from 1 to 365 or 366 in a leap year). | required |
Returns:
| Name | Type | Description |
|---|---|---|
solar_declination | float | The solar declination in radians for the given day of the year. |
Notes
The equation used here is a simple approximation and bases upon a direct translation from PVGIS' rsun3 source code:
- from file: rsun_base.cpp
- function: com_declin(no_of_day)
For more accurate calculations of solar position, comprehensive models like the Solar Position Algorithm (SPA) are typically used.
Source code in pvgisprototype/algorithms/jenco/solar_declination.py
@log_function_call
def calculate_solar_declination_series_jenco(
timestamps: DatetimeIndex,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarDeclination:
"""Approximate the sun's declination for a time series.
δ = arcsin (0.3978 sin (j’ - 1.4 + 0.0355 sin (j’ - 0.0489)))
The solar declination is the angle between the Sun's rays and the
equatorial plane of Earth. It varies throughout the year due to the tilt of
the Earth's axis and is an important parameter in determining the seasons
and the amount of solar radiation received at different latitudes.
The function calculates the `proportion` of the way through the year (in
radians), which is given by `(2 * pi * day_of_year) / 365.25`.
The `0.3978`, `1.4`, and `0.0355` are constants in the approximation
formula, with the `0.0489` being an adjustment factor for the slight
eccentricity of Earth's orbit.
Parameters
----------
day_of_year: int
The day of the year (ranging from 1 to 365 or 366 in a leap year).
Returns
-------
solar_declination: float
The solar declination in radians for the given day of the year.
Notes
-----
The equation used here is a simple approximation and bases upon a direct
translation from PVGIS' rsun3 source code:
- from file: rsun_base.cpp
- function: com_declin(no_of_day)
For more accurate calculations of solar position, comprehensive models like
the Solar Position Algorithm (SPA) are typically used.
"""
day_angle_series = calculate_day_angle_series_hofierka(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
solar_declination_series = numpy.arcsin(
0.3978
* numpy.sin(
day_angle_series.radians
- 1.4
+ eccentricity_amplitude
* numpy.sin(day_angle_series.radians - eccentricity_phase_offset)
)
)
if (
(solar_declination_series < SolarDeclination().min_radians)
| (solar_declination_series > SolarDeclination().max_radians)
).any():
out_of_range_values = solar_declination_series[
(solar_declination_series < SolarDeclination().min_radians)
| (solar_declination_series > SolarDeclination().max_radians)
]
# raise ValueError(# ?
logger.warning(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarDeclination().min_radians}, {SolarDeclination().max_radians}] radians"
f" in [code]solar_declination_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_declination_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarDeclination(
value=solar_declination_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.jenco,
solar_timing_algorithm="Jenčo",
)
solar_incidence ¶
Functions:
| Name | Description |
|---|---|
calculate_relative_longitude | Notes |
calculate_solar_incidence_series_jenco | Calculate the solar incidence angle between the position of the sun and |
calculate_relative_longitude ¶
calculate_relative_longitude(
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> RelativeLongitude
Notes
Hofierka (2002) uses equations presented by Jenčo (1992) :
tangent_relative_longitude =
(
- sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
In PVGIS' C source code, there is an error of one negative sign in either of the expressions! That is so because : cos(pi/2 + x) = -sin(x). As a consequence, the numerator becomes a positive number.
Source code :
/* These calculations depend on slope and aspect. Constant for the day if not tracking */
sin_phi_l = -gridGeom->coslat * cos_u * sin_v + gridGeom->sinlat * sin_u;
latid_l = asin(sin_phi_l);
cos_latid_l = cos(latid_l);
q1 = gridGeom->sinlat * cos_u * sin_v + gridGeom->coslat * sin_u;
tan_lam_l = - cos_u * cos_v / q1;
longit_l = atan (tan_lam_l);
if((aspect<M_PI)&&(longit_l<0.))
{
longit_l += M_PI;
}
else if((aspect>M_PI)&&(longit_l>0.))
{
longit_l -= M_PI;
}
Translation in to Python / pseudocode :
tangent_relative_longitude =
(
- cos(half_pi - surface_tilt) # cos(pi/2 - x) = sin(x)
* cos(half_pi + surface_orientation) # cos(pi/2 + x) = -sin(x) #
) / (
sin(latitude)
* cos(half_pi - surface_tilt)
* sin(half_pi + surface_orientation) # sin(pi/2 + x) = cos(x)
+ cos(latitude)
* sin(half_pi - surface_tilt) # sin(pi/2 - x) = cos(x)
)
As a consequence, PVGIS is like (note the positive numerator!) :
tangent_relative_longitude =
(
sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
Source code in pvgisprototype/algorithms/jenco/solar_incidence.py
@log_function_call
@validate_with_pydantic(CalculateRelativeLongitudeInputModel)
def calculate_relative_longitude(
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> RelativeLongitude:
"""
Notes
-----
Hofierka (2002) uses equations presented by Jenčo (1992) :
tangent_relative_longitude =
(
- sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
In PVGIS' C source code, there is an error of one negative sign in either
of the expressions! That is so because : cos(pi/2 + x) = -sin(x).
As a consequence, the numerator becomes a positive number.
Source code :
/* These calculations depend on slope and aspect. Constant for the day if not tracking */
sin_phi_l = -gridGeom->coslat * cos_u * sin_v + gridGeom->sinlat * sin_u;
latid_l = asin(sin_phi_l);
cos_latid_l = cos(latid_l);
q1 = gridGeom->sinlat * cos_u * sin_v + gridGeom->coslat * sin_u;
tan_lam_l = - cos_u * cos_v / q1;
longit_l = atan (tan_lam_l);
if((aspect<M_PI)&&(longit_l<0.))
{
longit_l += M_PI;
}
else if((aspect>M_PI)&&(longit_l>0.))
{
longit_l -= M_PI;
}
Translation in to Python / pseudocode :
tangent_relative_longitude =
(
- cos(half_pi - surface_tilt) # cos(pi/2 - x) = sin(x)
* cos(half_pi + surface_orientation) # cos(pi/2 + x) = -sin(x) #
) / (
sin(latitude)
* cos(half_pi - surface_tilt)
* sin(half_pi + surface_orientation) # sin(pi/2 + x) = cos(x)
+ cos(latitude)
* sin(half_pi - surface_tilt) # sin(pi/2 - x) = cos(x)
)
As a consequence, PVGIS is like (note the positive numerator!) :
tangent_relative_longitude =
(
sin(surface_tilt)
* sin(surface_orientation)
) / (
sin(latitude)
* sin(surface_tilt)
* cos(surface_orientation)
+ cos(latitude)
* cos(surface_tilt)
)
"""
# -----------------------------------------------------------------------
# in PVGIS an extra minus sign results to an all positive numerator!
# tangent_relative_longitude_numerator = sin(surface_tilt.radians) * sin(
# surface_orientation.radians
# )
# -----------------------------------------------------------------------
tangent_relative_longitude_numerator = -(
sin(surface_tilt.radians) * sin(surface_orientation.radians)
)
tangent_relative_longitude_denominator = -(
sin(latitude.radians)
* sin(surface_tilt.radians)
* cos(surface_orientation.radians)
+ cos(latitude.radians) * cos(surface_tilt.radians)
)
# force dtype !
relative_longitude = np.array(
[tangent_relative_longitude_numerator / tangent_relative_longitude_denominator],
dtype=dtype,
)
log_data_fingerprint(
data=relative_longitude,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return RelativeLongitude(
value=relative_longitude,
unit=RADIANS,
)
calculate_solar_incidence_series_jenco ¶
calculate_solar_incidence_series_jenco(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo | None,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
surface_in_shade_series: NpNDArray | None = None,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence
Calculate the solar incidence angle between the position of the sun and a reference solar surface.
Calculate the solar incidence angle based on the position of the sun (sun-vector) and the a reference solar surface. Typically the solar incidence is the angle between the sun-vector and the normal to the solar surface (surface-normal). However, the underlying functions that convert horizontal irradiance components to inclined, expect the complementary incidence angle which is defined as the angle between the sun-vector and the inclination or plane of the reference solar surface (surface-plane). We call this the "complementary" incidence angle contrasting typical definitions of the incidence angle between the sun-vector and the normal to the surface in question. Alternatively the function can return the angle between the sun-vector and the normal vector to the reference surface.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | Longitude in degrees | required |
latitude | float | Latitude in degrees | required |
surface_orientation | float | Orientation of the surface (azimuth angle in degrees) | SURFACE_ORIENTATION_DEFAULT |
surface_tilt | float | Tilt of the surface in degrees | SURFACE_TILT_DEFAULT |
Returns:
| Type | Description |
|---|---|
ndarray | Solar incidence angle or NO_SOLAR_INCIDENCE series if a shadow is detected. |
Notes
The solar incidence angle is the single most important quantity in the solar geometry setup. Its definition will affect all subsequent operations excluding none of the irradiance components, nor the photovoltaic power or energy estimations.
Attention! There is no one, and only one, definition of the solar incidence angle! While many authors refer to the angle between the sun-vector and the normal to the reference surface, for example Martin and Ruiz (2002, 2005), others, like for example Jenčo (1992) and Hofierka (2002), consider as incidence the angle between the sun-vector and the reference surface-plane. These angles are complementary to each other. This fact is an important one when treating trigonometric relationships that affect the calculation of the incidence angle.
In this program, we implement and consider as complementary the incidence angle as defined by Jenčo (1992) between the direction of the sun-rays and the inclination angle, or plane, of a reference surface. Hence, the default modus of this function returns the complementary incidence angle.
Following is meant to visualise a flat horizontal surface and the direction of a sun-ray. The angle between the two is what we call complementary.
____
Optionally, the function can return the typical incidence angle between the direction of the sun-rays and the normal direction to the reference surface in question.
Following means to visualise the same direction of a sun-ray and then the normal (vertical here) direction to the flat horizontal surface. The angle between the sun-ray and the normal (vertical here) direction is the typical definition of the solar incidence angle. This angle can be generated via the complementary_incidence_angle flag.
\ |
\ |
\|
\|
Reading the paper by Jenčo (1992) [1]_ (last page is a summary translation in English):
Orientation of georelief An with respect to Cardinal points and slopes
γN of georelief in the direction of slope curves in the given point on
the georelief with the latitude φ determines the latitude φ' and in
relation to longitude λ of this point also the relative longitude λ' of
the contact point of contact plane to reference spheric surface of the
Earth with identical course of the insolation (Fig. 1). Thus sinus of
insolation angle δexp on georelief for the local time moment T can be
expressed by modification of a relation that is well known in astronomy
for the calculation of sinus of solar altitude h0 a form of equation
(3). Latitude φ' and longitude λ' in the equation (3) is definitely
determined on the basis of transformation equations (4.3.31) in the
work [5] in dependence on the assessment of the basic direction of
orientation of georelief AN with respect to the cardinal points by
goniometric equations (5), (6) or (9), (10). After determination of the
hour of sunrise (Tv)s from shadow to light and hour of sunset (Tr)s
from light to shadow by equations (12), (13) abstracting also from hill
shading of georelief, total quantity of direct solar irradiance Qd on
the unit of area of georelief for one day under blue sky conditions can
be expressed by equation (14).
Although Hofierka (2002) does not explicitly state "the angle between the sun-vector and the plane to the surface," he borrows the equation defined by Jenčo (1992) who measures the incidence angle between the sun-vector and plane of the reference solar surface. Care needs to be taken when comparing or using other definitions of the solar incidence angle.
An inclined surface is defined by the inclination angle (also known as slope or tilt) γN and the azimuth (also known as aspect or orientation) AN. The latter is the angle between the projection of the normal on the horizontal surface and East.
tg(λ') = - (sin(γN) * sin(AN)) / (sin(ϕ) * sin(γN) * cos(AN) + cos(ϕ) * cos(γN)) sin(ϕ') = - cos(ϕ) * sin(γN) * cos(AN) + sin(ϕ) * cos(γN) C'31 = cos ϕ' cos δ C'33 = sin ϕ' sin δ
From PVGIS' C++ source code:
The timeAngle (which is the solar hour angle) in rsun_standalone_hourly_opt.cpp is calculated via:
`sunGeom->timeAngle = (solarTimeOfDay - 12) * HOURANGLE;`
where HOUR_ANGLE is defined as HOUR_ANGLE = pi / 12.0 which is the conversion factor to radians (π / 12 = 0.261799) and hence this is the equation to calculate the hour angle in radians, e.g., as per NOAA's equation:
`solar_hour_angle = (true_solar_time - 720) * (pi / 720)`.
in which 720 is minutes, whereas 60 is hours in PVGIS' C++ code.
- Shadow check not implemented.
Source code in pvgisprototype/algorithms/jenco/solar_incidence.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSolarIncidenceTimeSeriesJencoInputModel)
def calculate_solar_incidence_series_jenco(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo | None,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
surface_in_shade_series: NpNDArray | None = None,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence:
"""Calculate the solar incidence angle between the position of the sun and
a reference solar surface.
Calculate the solar incidence angle based on the position of the sun
(sun-vector) and the a reference solar surface. Typically the solar
incidence is the angle between the sun-vector and the normal to the solar
surface (surface-normal). However, the underlying functions that convert
horizontal irradiance components to inclined, expect the complementary
incidence angle which is defined as the angle between the sun-vector and
the inclination or plane of the reference solar surface (surface-plane).
We call this the "complementary" incidence angle contrasting typical
definitions of the incidence angle between the sun-vector and the normal to
the surface in question. Alternatively the function can return the angle
between the sun-vector and the normal vector to the reference surface.
Parameters
----------
longitude : float
Longitude in degrees
latitude : float
Latitude in degrees
surface_orientation : float
Orientation of the surface (azimuth angle in degrees)
surface_tilt : float
Tilt of the surface in degrees
Returns
-------
ndarray
Solar incidence angle or NO_SOLAR_INCIDENCE series if a shadow is detected.
Notes
-----
The solar incidence angle is the single most important quantity in the
solar geometry setup. Its definition will affect all subsequent operations
excluding none of the irradiance components, nor the photovoltaic power or
energy estimations.
Attention! There is no one, and only one, definition of the solar incidence
angle! While many authors refer to the angle between the sun-vector and the
normal to the reference surface, for example Martin and Ruiz (2002, 2005),
others, like for example Jenčo (1992) and Hofierka (2002), consider as
incidence the angle between the sun-vector and the reference surface-plane.
These angles are complementary to each other. This fact is an important one
when treating trigonometric relationships that affect the calculation of
the incidence angle.
In this program, we implement and consider as _complementary_ the incidence
angle as defined by Jenčo (1992) between the direction of the sun-rays and
the inclination angle, or plane, of a reference surface. Hence, the default
modus of this function returns the _complementary_ incidence angle.
Following is meant to visualise a flat horizontal surface and the
direction of a sun-ray. The angle between the two is what we call
complementary.
\
\
____\
Optionally, the function can return the _typical_ incidence angle between
the direction of the sun-rays and the normal direction to the reference
surface in question.
Following means to visualise the same direction of a sun-ray and then the
normal (vertical here) direction to the flat horizontal surface. The angle
between the sun-ray and the normal (vertical here) direction is the
_typical_ definition of the solar incidence angle. This angle can be
generated via the `complementary_incidence_angle` flag.
\ |
\ |
\|
-----
Reading the paper by Jenčo (1992) [1]_ (last page is a summary translation in
English):
Orientation of georelief An with respect to Cardinal points and slopes
γN of georelief in the direction of slope curves in the given point on
the georelief with the latitude φ determines the latitude φ' and in
relation to longitude λ of this point also the relative longitude λ' of
the contact point of contact plane to reference spheric surface of the
Earth with identical course of the insolation (Fig. 1). Thus sinus of
insolation angle δexp on georelief for the local time moment T can be
expressed by modification of a relation that is well known in astronomy
for the calculation of sinus of solar altitude h0 a form of equation
(3). Latitude φ' and longitude λ' in the equation (3) is definitely
determined on the basis of transformation equations (4.3.31) in the
work [5] in dependence on the assessment of the basic direction of
orientation of georelief AN with respect to the cardinal points by
goniometric equations (5), (6) or (9), (10). After determination of the
hour of sunrise (Tv)s from shadow to light and hour of sunset (Tr)s
from light to shadow by equations (12), (13) abstracting also from hill
shading of georelief, total quantity of direct solar irradiance Qd on
the unit of area of georelief for one day under blue sky conditions can
be expressed by equation (14).
Although Hofierka (2002) does not explicitly state "the angle between the
sun-vector and the plane to the surface," he borrows the equation defined
by Jenčo (1992) who measures the incidence angle between the sun-vector and
plane of the reference solar surface. Care needs to be taken when comparing
or using other definitions of the solar incidence angle.
An inclined surface is defined by the inclination angle (also known as
slope or tilt) `γN` and the azimuth (also known as aspect or orientation)
`AN`. The latter is the angle between the projection of the normal on the
horizontal surface and East.
tg(λ') = - (sin(γN) * sin(AN)) / (sin(ϕ) * sin(γN) * cos(AN) + cos(ϕ) * cos(γN))
sin(ϕ') = - cos(ϕ) * sin(γN) * cos(AN) + sin(ϕ) * cos(γN)
C'31 = cos ϕ' cos δ
C'33 = sin ϕ' sin δ
From PVGIS' C++ source code:
The `timeAngle` (which is the solar hour
angle) in `rsun_standalone_hourly_opt.cpp` is calculated via:
`sunGeom->timeAngle = (solarTimeOfDay - 12) * HOURANGLE;`
where HOUR_ANGLE is defined as `HOUR_ANGLE = pi / 12.0`
which is the conversion factor to radians (π / 12 = 0.261799)
and hence this is the equation to calculate the hour angle in radians,
e.g., as per NOAA's equation:
`solar_hour_angle = (true_solar_time - 720) * (pi / 720)`.
in which 720 is minutes, whereas 60 is hours in PVGIS' C++ code.
- Shadow check not implemented.
"""
# Identify times without solar insolation
solar_altitude_series = calculate_solar_altitude_series_jenco(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=0,
log=log,
)
solar_azimuth_east_based_series = calculate_solar_azimuth_series_jenco(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
dtype=dtype,
array_backend=array_backend,
verbose=0,
log=log,
) # Origin ?
# from pvgisprototype import SolarAzimuth
# solar_azimuth_series = SolarAzimuth(
# value=(solar_azimuth_east_based_series.value + np.pi/2),
# unit=RADIANS,
# )
# Prepare relevant quantities
sine_relative_inclined_latitude = -cos(latitude.radians) * sin(
surface_tilt.radians
) * cos(surface_orientation.radians) + sin(latitude.radians) * cos(
surface_tilt.radians
)
relative_inclined_latitude = asin(sine_relative_inclined_latitude)
solar_declination_series = calculate_solar_declination_series_jenco(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
log=log,
)
c_inclined_31_series = cos(relative_inclined_latitude) * np.cos(
solar_declination_series.radians
)
c_inclined_33_series = sin(relative_inclined_latitude) * np.sin(
solar_declination_series.radians
)
solar_hour_angle_series = calculate_solar_hour_angle_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
log=log,
)
relative_longitude = calculate_relative_longitude(
latitude=latitude,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
dtype=dtype,
log=log,
)
# Note the - in front of the solar_hour_angle_series ! Explain-Me !
sine_solar_incidence_series = (
c_inclined_31_series
* np.cos(solar_hour_angle_series.radians - relative_longitude.radians)
+ c_inclined_33_series
)
solar_incidence_series = np.arcsin(sine_solar_incidence_series)
incidence_angle_definition = SolarIncidence().definition_complementary
incidence_angle_description = SolarIncidence().description_complementary
if not complementary_incidence_angle:
logger.debug(
f":information: Converting solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}...",
alt=f":information: [bold][magenta]Converting[/magenta] solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}[/bold]...",
)
solar_incidence_series = (pi / 2) - solar_incidence_series
incidence_angle_definition = SolarIncidence().definition
incidence_angle_description = SolarIncidence().description
# Combined mask for no solar incidence, negative solar incidence or below horizon angles
mask_below_horizon_or_in_shade = (
solar_altitude_series.radians < 0
) | surface_in_shade_series.value
mask_no_solar_incidence_series = (
solar_incidence_series < 0
) | mask_below_horizon_or_in_shade
if zero_negative_solar_incidence_angle:
solar_incidence_series = np.where(
mask_no_solar_incidence_series,
# (solar_incidence_series < 0) | (solar_altitude_series.value < 0),
NO_SOLAR_INCIDENCE,
solar_incidence_series,
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_incidence_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarIncidence(
value=solar_incidence_series,
unit=RADIANS,
solar_positioning_algorithm=solar_declination_series.solar_positioning_algorithm, #
solar_timing_algorithm=solar_hour_angle_series.solar_timing_algorithm, #
incidence_algorithm=SolarIncidenceModel.jenco,
definition=incidence_angle_definition,
description=incidence_angle_description,
# azimuth_origin=solar_azimuth_east_based_series.origin,
azimuth_origin=None,
)
martin_ruiz ¶
Modules:
| Name | Description |
|---|---|
reflectivity | |
reflectivity ¶
Functions:
| Name | Description |
|---|---|
calculate_reflectivity_effect | Calculate absolute reflectivity effect |
calculate_reflectivity_effect_percentage | |
calculate_reflectivity_factor_for_direct_irradiance_series | Calculate the angular loss factor for the direct horizontal radiation |
calculate_reflectivity_factor_for_nondirect_irradiance | Calculate the reflectivity factor as for small angles of solar |
calculate_reflectivity_effect ¶
Calculate absolute reflectivity effect
The total loss due to the reflectivity effect (which depends on the solar incidence angle) as the difference of the irradiance after and before.
Notes
Other ideas :
-
Sum of reflectivity effect as a difference between the irradiance time series after and before the reflectivity effect, i.e. :
return reflectivity_effect = np.nansum(irradiance_series * reflectivity - irradiance_series) # Total lost energy due to AOI over the period
-
Average reflectivity effect :
REFLECTIVITY_EFFECT_AVERAGE_COLUMN_NAME: np.nanmean(calculate_reflectivity_effect(inclined_irradiance_series, reflectivity_factor_series)),
Source code in pvgisprototype/algorithms/martin_ruiz/reflectivity.py
@log_function_call
def calculate_reflectivity_effect(
irradiance,
reflectivity_factor,
):
"""Calculate absolute reflectivity effect
The total loss due to the reflectivity effect (which depends on the solar
incidence angle) as the difference of the irradiance after and before.
Notes
-----
Other ideas :
- Sum of reflectivity effect as a difference between the irradiance time
series after and before the reflectivity effect, i.e. :
return reflectivity_effect = np.nansum(irradiance_series * reflectivity - irradiance_series) # Total lost energy due to AOI over the period
- Average reflectivity effect :
REFLECTIVITY_EFFECT_AVERAGE_COLUMN_NAME: np.nanmean(calculate_reflectivity_effect(inclined_irradiance_series, reflectivity_factor_series)),
"""
effect = (irradiance * reflectivity_factor) - irradiance
return np.atleast_1d(np.nan_to_num(effect, nan=0)) # Force 1D array output, safer output ?
calculate_reflectivity_effect_percentage ¶
Source code in pvgisprototype/algorithms/martin_ruiz/reflectivity.py
@log_function_call
def calculate_reflectivity_effect_percentage(
irradiance,
reflectivity,
):
""" """
# --------------------------------------------------- Is this safe ? -
with np.errstate(divide="ignore", invalid="ignore"):
percentage = np.where(
irradiance != 0,
100 * (1 - ((irradiance * reflectivity) / irradiance)),
0,
)
return percentage
calculate_reflectivity_factor_for_direct_irradiance_series ¶
calculate_reflectivity_factor_for_direct_irradiance_series(
solar_incidence_series: SolarIncidence,
angular_loss_coefficient: float = ANGULAR_LOSS_COEFFICIENT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
)
Calculate the angular loss factor for the direct horizontal radiation based on the solar incidence angle.
This function implements the solar incidence angle modifier as per Martin & Ruiz (2005). Expected is the angle between the sun-solar-surface vector and the vector normal to the reference solar surface. We call this the typical incidence angle as opposed to the complementary incidence angle defined by Jenčo (1992).
The adjustment factor represents the fraction of the original direct_radiation that is retained after accounting for the loss of radiation due to the angle of incidence or the orientation of the surface with respect to the sun.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solar_incidence_series | float | Solar incidence angle series | required |
angular_loss_coefficient | float | | ANGULAR_LOSS_COEFFICIENT |
Returns:
| Name | Type | Description |
|---|---|---|
reflectivity_factor_series | float | |
Notes
The adjustment involves:
-
computes the fraction of radiation that is not lost due to the
solar_incidence_angleangle divided by thesolar_declinationranging between 0 (complete loss) and 1 (no loss):( 1 - exp( -solar_incidence_angle / angle_of_incidence_constant ) )-
The exponential function
exp, raises the mathematical constante(approximately 2.71828) to the power of the given argument. -
The negative exponential term of the fraction
solar_altitude / solar_declinationcalculates the exponential decay or attenuation factor based on the ratio ofsolar_altitudeto thesolar_declination.
-
-
rescales the adjusted value to bring it within a suitable range, by multiplying it by the reciprocal of the exponential term with the reciprocal of the
solar_declination:1 / ( 1 - exp( - 1 / solar_declination) )
ensuring no excessive amplification or diminishing the effect (over-amplification or under-amplification).
The paper :
- -
| 1 - exp( -cos(a) / a_r ) |
1 - | -------------------------- |
| 1 - exp( -1 / a_r) |
| 1 - exp( -1 / a_r) |
In PVGIS :
``` c
angular_loss_denom = 1. / (1 - exp(-1. / a_r));
br *= (1 - exp(-sh / a_r)) * angular_loss_denom;
```
where:
- br : direct inclined irradiance
- sh : _sine_ of _complementary_ solar incidence angle (as per Jenčo, 1992)
- a_r : angular loss coefficient
In pvlib :
``` python
with np.errstate(invalid='ignore'):
iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
```
where:
- iam : incidence angle modifier
- aoi : angle of incidence
- a_r : angular loss coefficient
Review Me : --------------------------------------------------------------
This function will return a time series incidence_angle_modifier_series of floating point numbers. As it may generate NaN elements, further processing in a time series context, would required attention for example when summing arrays with NaN elements.
To circumvent eventual "problems" and the need for special handling downstream in the code, we replace all NaN elements with 0 just before returning the final time series.
Source code in pvgisprototype/algorithms/martin_ruiz/reflectivity.py
@log_function_call
def calculate_reflectivity_factor_for_direct_irradiance_series(
solar_incidence_series: SolarIncidence,
angular_loss_coefficient: float = ANGULAR_LOSS_COEFFICIENT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
):
"""Calculate the angular loss factor for the direct horizontal radiation
based on the solar incidence angle.
This function implements the solar incidence angle modifier as per Martin &
Ruiz (2005). Expected is the angle between the sun-solar-surface vector and
the vector normal to the reference solar surface. We call this the
_typical_ incidence angle as opposed to the _complementary_ incidence angle
defined by Jenčo (1992).
The adjustment factor represents the fraction of the original
`direct_radiation` that is retained after accounting for the loss of
radiation due to the angle of incidence or the orientation of the surface
with respect to the sun.
Parameters
----------
solar_incidence_series : float
Solar incidence angle series
angular_loss_coefficient : float
Returns
-------
reflectivity_factor_series : float
Notes
-----
The adjustment involves:
1. computes the fraction of radiation that is _not_ lost due to
the `solar_incidence_angle` angle divided by the `solar_declination` ranging between
0 (complete loss) and 1 (no loss):
`( 1 - exp( -solar_incidence_angle / angle_of_incidence_constant ) )`
- The exponential function `exp`, raises the mathematical constant `e`
(approximately 2.71828) to the power of the given argument.
- The negative exponential term of the fraction `solar_altitude /
solar_declination` calculates the exponential decay or attenuation
factor based on the ratio of `solar_altitude` to the `solar_declination`.
2. rescales the adjusted value to bring it within a suitable range,
by multiplying it by the reciprocal of the exponential term with the
reciprocal of the `solar_declination`:
`1 / ( 1 - exp( - 1 / solar_declination) )`
ensuring no excessive amplification or diminishing the effect
(over-amplification or under-amplification).
The paper :
- -
| 1 - exp( -cos(a) / a_r ) |
1 - | -------------------------- |
| 1 - exp( -1 / a_r) |
- -
In PVGIS :
``` c
angular_loss_denom = 1. / (1 - exp(-1. / a_r));
br *= (1 - exp(-sh / a_r)) * angular_loss_denom;
```
where:
- br : direct inclined irradiance
- sh : _sine_ of _complementary_ solar incidence angle (as per Jenčo, 1992)
- a_r : angular loss coefficient
In pvlib :
``` python
with np.errstate(invalid='ignore'):
iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
```
where:
- iam : incidence angle modifier
- aoi : angle of incidence
- a_r : angular loss coefficient
Review Me : --------------------------------------------------------------
This function will return a time series `incidence_angle_modifier_series`
of floating point numbers. As it may generate NaN elements, further
processing in a time series context, would required attention for example
when summing arrays with NaN elements.
To circumvent eventual "problems" and the need for special handling
downstream in the code, we replace all NaN elements with 0 just before
returning the final time series.
"""
logger.debug(
f"> Executing solar radiation modelling function calculate_reflectivity_factor_for_direct_irradiance_series()",
alt=f"> Executing [underline]solar radiation modelling[/underline] function calculate_reflectivity_factor_for_direct_irradiance_series()",
)
try:
numerator = 1 - np.exp(
-np.cos(solar_incidence_series) / angular_loss_coefficient
)
denominator = 1 / (1 - exp(-1 / angular_loss_coefficient))
incidence_angle_modifier_series = numerator / denominator
incidence_angle_modifier_series = np.where(
np.abs(solar_incidence_series) >= pi / 2, 0, incidence_angle_modifier_series
) # Borrowed from pvlib !
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
if verbose > 0:
print(f"Incidence angle modifier series: {incidence_angle_modifier_series}")
log_data_fingerprint(
data=incidence_angle_modifier_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
logger.debug(
f" < Returning incidence angle modifier series :\n{incidence_angle_modifier_series}",
alt=f" [green]<[/green] Returning incidence angle modifier series :\n{incidence_angle_modifier_series}",
)
return incidence_angle_modifier_series
# Review-Me !
except ZeroDivisionError as e:
logger.error(f"Zero Division Error: {e}")
print("Error: Division by zero in calculating the angular loss factor.")
return np.array([1]) # Return an array with a single element as 1
calculate_reflectivity_factor_for_nondirect_irradiance ¶
calculate_reflectivity_factor_for_nondirect_irradiance(
indirect_angular_loss_coefficient,
angular_loss_coefficient=ANGULAR_LOSS_COEFFICIENT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
) -> float
Calculate the reflectivity factor as for small angles of solar incidence.
Notes
Review Me : --------------------------------------------------------------
This function will return a single incidence_angle_modifier (or call it loss factor) float number. Further processing in a time series context can be done by simply replicating the reflectivity factor for all timestamps.
Further, this structure will not generate any NaNs across a time series which often need special handling, i.e. when summing arrays with NaN elements.
Other implementations
In PVGIS v5.2 :
- AOIConstants[0]
- AOIConstants[1]
c1 = 4./(3.*M_PI); diff_coeff_angleloss = sinslope + (M_PI-sunSlopeGeom->slope - sinslope) / (1+cosslope); diff_loss_factor = 1. - exp( - ( c1 * diff_coeff_angleloss + AOIConstants[0] * diff_coeff_angleloss * diff_coeff_angleloss ) / AOIConstants[1]);
Source code in pvgisprototype/algorithms/martin_ruiz/reflectivity.py
@log_function_call
def calculate_reflectivity_factor_for_nondirect_irradiance(
indirect_angular_loss_coefficient,
angular_loss_coefficient=ANGULAR_LOSS_COEFFICIENT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
) -> float:
"""Calculate the reflectivity factor as for small angles of solar
incidence.
Notes
-----
Review Me : --------------------------------------------------------------
This function will return a single `incidence_angle_modifier` (or call it
loss factor) float number. Further processing in a time series context can
be done by simply replicating the reflectivity factor for all timestamps.
Further, this structure will not generate any NaNs across a time series
which often need special handling, i.e. when summing arrays with NaN
elements.
Other implementations
In PVGIS v5.2 :
- AOIConstants[0]
- AOIConstants[1]
c1 = 4./(3.*M_PI);
diff_coeff_angleloss = sinslope + (M_PI-sunSlopeGeom->slope - sinslope)
/
(1+cosslope);
diff_loss_factor =
1. - exp(
- ( c1 * diff_coeff_angleloss
+ AOIConstants[0]
* diff_coeff_angleloss
* diff_coeff_angleloss
) /
AOIConstants[1]);
"""
logger.debug(
f"> Executing solar radiation modelling function calculate_reflectivity_factor_for_nondirect_irradiance()",
alt=f"> Executing [underline]solar radiation modelling[/underline] function calculate_reflectivity_factor_for_nondirect_irradiance()",
)
angular_loss_coefficient_product = angular_loss_coefficient / 2 - 0.154
c1 = 4 / (3 * pi)
incidence_angle_modifier = 1 - exp(
-(
c1 * indirect_angular_loss_coefficient
+ angular_loss_coefficient_product * pow(angular_loss_coefficient, 2)
)
/ angular_loss_coefficient
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
logger.debug(
f" < Returning incidence angle modifier :\n{incidence_angle_modifier}",
alt=f" [green]<[/green] Returning incidence angle modifier :\n{incidence_angle_modifier}",
)
return incidence_angle_modifier
milne1921 ¶
Modules:
| Name | Description |
|---|---|
solar_time | |
solar_time ¶
Functions:
| Name | Description |
|---|---|
calculate_apparent_solar_time_milne1921 | Calculate the apparent solar time based on the equation of time by Milne 1921 |
calculate_apparent_solar_time_series_milne1921 | Calculate the apparent solar time based on the equation of time by Milne 1921 for a series of timestamps |
calculate_apparent_solar_time_milne1921 ¶
calculate_apparent_solar_time_milne1921(
longitude: Longitude,
timestamp: Timestamp,
verbose: int = 0,
) -> Timestamp
Calculate the apparent solar time based on the equation of time by Milne 1921
Notes
-
Local Time (LT)
-
Local Standard Time Meridian (LSTM)
-
1 hour in time equals to 15° degrees of earth's rotation (from: 360°/24 hours)
-
Examples:
- Sydney Australia is UTC +10 so the LSTM is 10 * 15° = 150 °E.
- Phoenix, USA is UTC -7 so the LSTM is -7 * 15° = -105°E or 105 °W
-
-
The equation of time (EoT) (in minutes) is an empirical equation that corrects for the eccentricity of the Earth's orbit and the Earth's axial tilt. An approximation accurate to within ½ minute is:
EoT = 9.87 * sin(2*B) - 7.53 * cos(B) - 1.5 * sin(B)
where:
- DeltaTUTC : Local time minus UTC, in hours, also equal to the time zone
- B = 360 / 365 * (day_of_year - 81) # in degrees
- Time correction (TC) factor = 4 * (longitude - LSTM) + EoT
-
Solar time (or local solar time) = LT + TC / 60
- The solar (or local) solar time here equals to :
- the apparent solar time (AST)
- or corrected local solar time
- or true solar time or (TST) as termed in other models/equations.
-
Hour angle = 15 * (LST - 12)
_Milne
@article{Milne1921, doi = {10.2307/3604631}, year = 1921, publisher = {Cambridge University Press ({CUP})}, volume = {10}, number = {155}, pages = {372--375}, author = {R. M. Milne}, title = {593. Note on the Equation of Time}, journal = {The Mathematical Gazette} }
See also:
@incollection{KALOGIROU201451, title = {Chapter 2 - Environmental Characteristics}, editor = {Soteris A. Kalogirou}, booktitle = {Solar Energy Engineering (Second Edition)}, publisher = {Academic Press}, edition = {Second Edition}, address = {Boston}, pages = {51-123}, year = {2014}, isbn = {978-0-12-397270-5}, doi = {https://doi.org/10.1016/B978-0-12-397270-5.00002-9}, url = {https://www.sciencedirect.com/science/article/pii/B9780123972705000029}, author = {Soteris A. Kalogirou}, keywords = {Atmospheric attenuation, Extraterrestrial radiation, Radiation exchange between surfaces, Shadow determination, Solar angles, Solar radiation measuring instruments, Solar radiation, Terrestrial irradiation, Total radiation on tilted surfaces, Typical meteorological year}, abstract = {Chapter 2 gives an analysis of the environmental characteristics of solar radiation and in particular the reckoning of time and solar angles. In the latter the basic solar geometry equations are given including declination, hour angle, altitude angle, azimuth angle as well as the incidence angle for stationary and moving surfaces, sun path diagrams, and shadow determination including the way to calculate shading effects. This is followed by a description of the basic principles of solar radiation heat transfer including transparent plates, radiation exchange between surfaces, extraterrestrial solar radiation, atmospheric attenuation, terrestrial irradiation, and total radiation on tilted surfaces. It concludes with a review of the solar radiation measuring instruments and the way to construct typical meteorological year files.} }
Source code in pvgisprototype/algorithms/milne1921/solar_time.py
@validate_with_pydantic(CalculateSolarTimeMilne1921InputModel)
def calculate_apparent_solar_time_milne1921(
longitude: Longitude,
timestamp: Timestamp,
verbose: int = 0,
) -> Timestamp:
"""Calculate the apparent solar time based on the equation of time by Milne 1921
Notes
-----
- Local Time (LT)
- Local Standard Time Meridian (LSTM)
- 1 hour in time equals to 15° degrees of earth's rotation
(from: 360°/24 hours)
- Examples:
- Sydney Australia is UTC +10 so the LSTM is 10 * 15° = 150 °E.
- Phoenix, USA is UTC -7 so the LSTM is -7 * 15° = -105°E or 105 °W
- The equation of time (EoT) (in minutes) is an empirical equation that
corrects for the eccentricity of the Earth's orbit and the Earth's axial
tilt. An approximation accurate to within ½ minute is:
EoT = 9.87 * sin(2*B) - 7.53 * cos(B) - 1.5 * sin(B)
where:
- DeltaTUTC : Local time minus UTC, in hours, also equal to the time zone
- B = 360 / 365 * (day_of_year - 81) # in degrees
- Time correction (TC) factor = 4 * (longitude - LSTM) + EoT
- Solar time (or local solar time) = LT + TC / 60
* The solar (or local) solar time here equals to :
- the apparent solar time (AST)
- or corrected local solar time
- or true solar time or (TST)
as termed in other models/equations.
- Hour angle = 15 * (LST - 12)
_Milne
@article{Milne1921,
doi = {10.2307/3604631},
year = 1921,
publisher = {Cambridge University Press ({CUP})},
volume = {10},
number = {155},
pages = {372--375},
author = {R. M. Milne},
title = {593. Note on the Equation of Time},
journal = {The Mathematical Gazette}
}
See also:
@incollection{KALOGIROU201451,
title = {Chapter 2 - Environmental Characteristics},
editor = {Soteris A. Kalogirou},
booktitle = {Solar Energy Engineering (Second Edition)},
publisher = {Academic Press},
edition = {Second Edition},
address = {Boston},
pages = {51-123},
year = {2014},
isbn = {978-0-12-397270-5},
doi = {https://doi.org/10.1016/B978-0-12-397270-5.00002-9},
url = {https://www.sciencedirect.com/science/article/pii/B9780123972705000029},
author = {Soteris A. Kalogirou},
keywords = {Atmospheric attenuation, Extraterrestrial radiation, Radiation exchange between surfaces, Shadow determination, Solar angles, Solar radiation measuring instruments, Solar radiation, Terrestrial irradiation, Total radiation on tilted surfaces, Typical meteorological year},
abstract = {Chapter 2 gives an analysis of the environmental characteristics of solar radiation and in particular the reckoning of time and solar angles. In the latter the basic solar geometry equations are given including declination, hour angle, altitude angle, azimuth angle as well as the incidence angle for stationary and moving surfaces, sun path diagrams, and shadow determination including the way to calculate shading effects. This is followed by a description of the basic principles of solar radiation heat transfer including transparent plates, radiation exchange between surfaces, extraterrestrial solar radiation, atmospheric attenuation, terrestrial irradiation, and total radiation on tilted surfaces. It concludes with a review of the solar radiation measuring instruments and the way to construct typical meteorological year files.}
}
"""
# # Handle Me during input validation? -------------------------------------
# if timezone != timestamp.tzinfo:
# try:
# timestamp = timestamp.astimezone(timezone)
# except Exception as e:
# logging.warning(f'Error setting tzinfo for timestamp = {timestamp}: {e}')
# # Handle Me during input validation? -------------------------------------
# Equation of Time, Milne 1921 -------------------------------------------
# The difference of local time from UTC equals the time zone, in hours
local_time_minus_utc = timestamp.utcoffset().total_seconds() / 3600
local_standard_time_meridian = 15 * local_time_minus_utc
# `b` for the equation of time -------------------------------------------
day_of_year = timestamp.timetuple().tm_yday
b = radians(360 / 365 * (day_of_year - 81)) # from degrees to radians
# however, in :
# Solar Energy Engineering (Second Edition),
# Chapter 2 - Environmental Characteristics,
# Soteris A. Kalogirou, Academic Press, 2014,
# ISBN 9780123972705, https://doi.org/10.1016/B978-0-12-397270-5.00002-9.
# Pages 51-123,
# b = (day_of_year - 81) * 360 / 364 (in degrees)
# -----------------------------------------------------------------------
equation_of_time = 9.87 * sin(2 * b) - 7.53 * cos(b) - 1.5 * sin(b)
# ------------------------------------------------------------------------
# the following equation requires longitude in degrees!
time_correction_factor = (
4 * (longitude.degrees - local_standard_time_meridian) + equation_of_time
)
# ------------------------------------------------------------------------
time_correction_factor_hours = time_correction_factor / 60
apparent_solar_time = timestamp + Timedelta(hours=time_correction_factor_hours)
if verbose > 0:
print("Day of year : {day_of_year}")
print("Equation of time : {equation_of_time}")
print("Time correction factor : {time_correction_factor}")
if verbose == 3:
debug(locals())
return TrueSolarTime(
value=apparent_solar_time,
unit=MINUTES,
timing_algorithm=SolarTimeModel.milne,
)
calculate_apparent_solar_time_series_milne1921 ¶
calculate_apparent_solar_time_series_milne1921(
longitude: Longitude,
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
)
Calculate the apparent solar time based on the equation of time by Milne 1921 for a series of timestamps
Source code in pvgisprototype/algorithms/milne1921/solar_time.py
@log_function_call
def calculate_apparent_solar_time_series_milne1921(
longitude: Longitude,
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
):
"""Calculate the apparent solar time based on the equation of time by Milne 1921 for a series of timestamps"""
# We need a timezone!
utc_zoneinfo = ZoneInfo("UTC")
local_standard_time_meridian_minutes_series = 0 # in UTC the offest is 0
if timestamps.tzinfo is None: # set to UTC
timestamps = timestamps.tz_localize(utc_zoneinfo)
elif timestamps.tz != utc_zoneinfo: # convert to UTC
timestamps = timestamps.tz_convert(utc_zoneinfo)
# # ------------------------------------------- Further Optimisation ?
# Optimisation : calculate unique offsets
unique_timezones = timestamps.map(lambda ts: ts.tzinfo)
unique_offsets = {
tz: tz.utcoffset().total_seconds() / 60 for tz in set(unique_timezones)
}
# Map offsets back to timestamps
local_standard_time_meridian_minutes_series = array(
[unique_offsets[tz] for tz in unique_timezones], dtype=dtype
)
# ------------------------------------------------- Further Optimisation ?
days_of_year = timestamps.dayofyear
days_in_years = get_days_in_years(timestamps.year)
# In the original equation : days_in_years = 365
b = numpy_radians(360 / days_in_years * (days_of_year - 81))
equation_of_time = (
9.87 * numpy_sin(2 * b) - 7.53 * numpy_cos(b) - 1.5 * numpy_sin(b)
)
# In the original equation :
# time_correction_factor = 4 * (longitude - local_standard_time_meridian) + equation_of_time in hours
time_correction_factor_minutes = (
longitude.as_minutes
- local_standard_time_meridian_minutes_series
+ equation_of_time
)
# time_correction_factor_hours = time_correction_factor / 60 # We are already in minutes !
true_solar_time_series = (
timestamps - timestamps.normalize()
).total_seconds() + time_correction_factor_minutes * 60
# array_parameters = {
# "shape": true_solar_time_series.shape,
# "dtype": dtype,
# "init_method": "zeros",
# "backend": array_backend,
# }
# true_solar_time_series_in_minutes = create_array(**array_parameters)
true_solar_time_series_in_minutes = numpy_mod(
true_solar_time_series.astype(dtype) / 60, 1440
)
if validate_output:
if not (
(TrueSolarTime().min_minutes <= true_solar_time_series_in_minutes)
& (true_solar_time_series_in_minutes <= TrueSolarTime().max_minutes)
).all():
out_of_range_values = true_solar_time_series_in_minutes[
~(
(TrueSolarTime().min_minutes <= true_solar_time_series_in_minutes)
& (true_solar_time_series_in_minutes <= TrueSolarTime().max_minutes)
)
]
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{TrueSolarTime().min_minutes}, {TrueSolarTime().max_minutes}] minutes"
f" in [code]true_solar_time_series_in_minutes[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=true_solar_time_series_in_minutes,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return TrueSolarTime(
value=array(true_solar_time_series_in_minutes, dtype=dtype),
unit=MINUTES,
timing_algorithm=SolarTimeModel.milne,
)
muneer ¶
Modules:
| Name | Description |
|---|---|
irradiance | |
irradiance ¶
Modules:
| Name | Description |
|---|---|
diffuse | |
diffuse ¶
Modules:
| Name | Description |
|---|---|
clear_sky | |
in_shade | |
inclined | |
potentially_sunlit | |
sky_irradiance | |
sunlit | |
term_n | |
clear_sky ¶
Modules:
| Name | Description |
|---|---|
inclined | |
inclined ¶
Functions:
| Name | Description |
|---|---|
calculate_clear_sky_diffuse_inclined_irradiance_muneer | Calculate the diffuse irradiance incident on a solar surface. |
calculate_clear_sky_diffuse_inclined_irradiance_muneer ¶calculate_clear_sky_diffuse_inclined_irradiance_muneer(
elevation: float,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex | None = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo | None = ZoneInfo("UTC"),
global_horizontal_irradiance_series: (
ndarray | None
) = None,
direct_horizontal_irradiance_series: (
ndarray | None
) = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: (
SolarAzimuth | None
) = SolarAzimuth(),
solar_incidence_series: SolarIncidence | None = None,
surface_in_shade_series: NpNDArray | None = None,
shading_states: List[ShadingState] = [all],
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedInclinedIrradiance
Calculate the diffuse irradiance incident on a solar surface.
Notes
In order or appearance:
- extraterrestrial_normal_irradiance : G0
- extraterrestrial_horizontal_irradiance : G0h = G0 sin(h0)
- kb : Proportion between direct (beam) and extraterrestrial irradiance : Kb
- diffuse_horizontal_component : Dhc [W.m-2]
- diffuse_transmission_function() :
- linke_turbidity_factor :
- diffuse_solar_altitude_function() :
- solar_altitude :
- calculate_term_n():
- n : the N term
- diffuse_sky_irradiance()
- sine_solar_incidence_angle
- sine_solar_altitude
- diffuse_sky_irradiance
- calculate_diffuse_sky_irradiance() : F(γN)
- surface_tilt :
- diffuse_inclined_irradiance :
- diffuse_horizontal_component :
- azimuth_difference :
- solar_azimuth :
- surface_orientation :
- diffuse_irradiance
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/clear_sky/inclined.py
@log_function_call
@custom_cached
def calculate_clear_sky_diffuse_inclined_irradiance_muneer(
elevation: float,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex | None = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo | None = ZoneInfo('UTC'),
global_horizontal_irradiance_series: ndarray | None = None,
direct_horizontal_irradiance_series: ndarray | None = None,
linke_turbidity_factor_series: LinkeTurbidityFactor = LinkeTurbidityFactor(),
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = SolarAzimuth(),
solar_incidence_series: SolarIncidence | None = None,
surface_in_shade_series: NpNDArray | None = None,
shading_states: List[ShadingState] = [ShadingState.all],
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedInclinedIrradiance:
"""Calculate the diffuse irradiance incident on a solar surface.
Notes
-----
In order or appearance:
- extraterrestrial_normal_irradiance : G0
- extraterrestrial_horizontal_irradiance : G0h = G0 sin(h0)
- kb : Proportion between direct (beam) and extraterrestrial irradiance : Kb
- diffuse_horizontal_component : Dhc [W.m-2]
- diffuse_transmission_function() :
- linke_turbidity_factor :
- diffuse_solar_altitude_function() :
- solar_altitude :
- calculate_term_n():
- n : the N term
- diffuse_sky_irradiance()
- sine_solar_incidence_angle
- sine_solar_altitude
- diffuse_sky_irradiance
- calculate_diffuse_sky_irradiance() : F(γN)
- surface_tilt :
- diffuse_inclined_irradiance :
- diffuse_horizontal_component :
- azimuth_difference :
- solar_azimuth :
- surface_orientation :
- diffuse_irradiance
"""
if (
global_horizontal_irradiance_series is not None
and direct_horizontal_irradiance_series is not None
):
logger.error(
":information: The global_horizontal_irradiance and/or direct_horizontal_irradiance inputs should be None at this point !",
alt = ":information: [bold red]The [code]global_horizontal_irradiance[/code] and/or [code]direct_horizontal_irradiance[/code] inputs should be [code]None[/code] at this point ![/bold red]",
)
raise ValueError(
":information: The `global_horizontal_irradiance` and/or `direct_horizontal_irradiance` inputs should be `None` at this point !",
)
else: # model the diffuse horizontal irradiance
if verbose > 0:
logger.info(
":information: Modelling clear-sky diffuse horizontal irradiance ...",
alt=":information: [bold]Modelling[/bold] clear-sky diffuse horizontal irradiance ...",
)
# create a placeholder array for global horizontal irradiance
global_horizontal_irradiance_series = create_array(
timestamps.shape, dtype=dtype, init_method=np.nan, backend=array_backend
)
# in which case, however and if NOT read from external series already,
# we need the direct component for the kb series
direct_horizontal_irradiance_series = (
calculate_clear_sky_direct_horizontal_irradiance_hofierka(
elevation=elevation,
timestamps=timestamps,
solar_altitude_series=solar_altitude_series,
surface_in_shade_series=surface_in_shade_series,
linke_turbidity_factor_series=linke_turbidity_factor_series,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
)
diffuse_horizontal_irradiance_series = (
calculate_clear_sky_diffuse_horizontal_irradiance_hofierka(
timestamps=timestamps,
linke_turbidity_factor_series=linke_turbidity_factor_series,
solar_altitude_series=solar_altitude_series,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
)
#
# ----------------------------------- Diffuse Horizontal Irradiance -- ^^^
# At this point, the diffuse_horizontal_irradiance_series are either :
# calculated from external time series Or modelled
# Initialise shading_state_series to avoid the "UnboundLocalError"
shading_state_series = create_array(
timestamps.shape, dtype='object', init_method="empty", backend=array_backend
)
nan_series = create_array(
timestamps.shape, dtype=dtype, init_method=np.nan, backend=array_backend
)
unset_series = create_array(
timestamps.shape, dtype=dtype, init_method="unset", backend=array_backend
)
# np.full(timestamps.shape, "Unset", "object")
if surface_tilt <= surface_tilt_horizontally_flat_panel_threshold:
if verbose > 0:
logger.info(
":information: Modelling clear-sky diffuse inclined irradiance for a horizontally flat panel...",
alt=":information: [bold]Modelling[/bold] clear-sky diffuse inclined irradiance for a horizontally flat panel...",
)
diffuse_inclined_irradiance_series = np.copy(
diffuse_horizontal_irradiance_series.value
)
# to not break the output !
diffuse_sky_irradiance_series = unset_series
n_series = unset_series
kb_series = unset_series
azimuth_difference_series = unset_series
solar_incidence_series = SolarIncidence(
value=unset_series,
incidence_algorithm=NOT_AVAILABLE,
definition=NOT_AVAILABLE,
azimuth_origin=NOT_AVAILABLE,
)
extraterrestrial_normal_irradiance_series = ExtraterrestrialNormalIrradiance(
value=unset_series,
unit=NOT_AVAILABLE,
day_angle=unset_series,
solar_constant=None,
eccentricity_phase_offset=None,
eccentricity_amplitude=None,
distance_correction_factor=None,
)
extraterrestrial_horizontal_irradiance_series = ExtraterrestrialHorizontalIrradiance(
value=unset_series,
unit=NOT_AVAILABLE,
day_angle=unset_series,
solar_constant=None,
eccentricity_phase_offset=None,
eccentricity_amplitude=None,
distance_correction_factor=None,
)
else: # tilted (or inclined) surface
# Note: in PVGIS: if surface_orientation != 'UNDEF' and surface_tilt != 0:
# --------------------------------------------------- Is this safe ? -
# Calculate or get quantities required : ---------------------------- >>> >>> >>>
# 1. to model the diffuse horizontal irradiance [optional]
# 2. to calculate the diffuse sky ... to consider shaded, sunlit and potentially sunlit surfaces
#
if verbose > 0:
logger.info(
":information: Modelling clear-sky diffuse inclined irradiance for a tilted panel...",
alt=":information: [bold]Modelling[/bold] clear-sky diffuse inclined irradiance for a tilted panel...",
)
extraterrestrial_normal_irradiance_series = (
calculate_extraterrestrial_normal_irradiance_hofierka(
timestamps=timestamps,
solar_constant=solar_constant,
eccentricity_phase_offset=eccentricity_phase_offset,
eccentricity_amplitude=eccentricity_amplitude,
dtype=dtype,
array_backend=array_backend,
verbose=0, # no verbosity here by choice!
log=log,
)
)
extraterrestrial_horizontal_irradiance_series = calculate_extraterrestrial_horizontal_irradiance_series_hofierka(
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
solar_altitude_series=solar_altitude_series,
)
#
# Calculate quantities required : ---------------------------- <<< <<< <<<
with np.errstate(divide="ignore", invalid="ignore"):
kb_series = ( # proportion between direct and extraterrestrial
direct_horizontal_irradiance_series.value
/ extraterrestrial_horizontal_irradiance_series.value
)
n_series = calculate_term_n_series_hofierka(
kb_series,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
)
diffuse_sky_irradiance_series = where(
np.isnan(n_series), # handle NaN cases
0,
calculate_diffuse_sky_irradiance_series_hofierka(
n_series=n_series,
surface_tilt=surface_tilt,
),
)
# prepare size of output array!
diffuse_inclined_irradiance_series = create_array(
timestamps.shape, dtype=dtype, init_method="zeros", backend=array_backend
)
# prepare cases : surfaces in shade, sunlit, potentially sunlit
from pvgisprototype.api.position.models import select_models
shading_states = select_models(
ShadingState, shading_states
) # Using a callback fails!
diffuse_inclined_irradiance_series = calculate_diffuse_inclined_irradiance_in_shade(
# mask_surface_in_shade_series=mask_surface_in_shade_series,
solar_incidence=solar_incidence_series,
surface_in_shade=surface_in_shade_series,
shading_states=shading_states,
shading_state_series=shading_state_series,
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
timestamps=timestamps,
surface_tilt=surface_tilt,
diffuse_inclined_irradiance=diffuse_inclined_irradiance_series,
diffuse_horizontal_irradiance=diffuse_horizontal_irradiance_series,
)
diffuse_inclined_irradiance_series = calculate_diffuse_inclined_irradiance_sunlit(
solar_incidence=solar_incidence_series,
shading_states=shading_states,
solar_altitude=solar_altitude_series,
shading_state_series=shading_state_series,
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
kb_series=kb_series,
diffuse_inclined_irradiance=diffuse_inclined_irradiance_series,
diffuse_horizontal_irradiance=diffuse_horizontal_irradiance_series,
)
#
azimuth_difference_series = NOT_AVAILABLE # not always required, set to avoid UnboundLocalError!
#
diffuse_inclined_irradiance_series = calculate_diffuse_inclined_irradiance_potentially_sunlit(
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
solar_azimuth=solar_azimuth_series,
shading_states=shading_states,
solar_altitude=solar_altitude_series,
shading_state_series=shading_state_series,
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
kb_series=kb_series,
diffuse_inclined_irradiance=diffuse_inclined_irradiance_series,
diffuse_horizontal_irradiance=diffuse_horizontal_irradiance_series,
)
# Replace None with a placeholder -- this is important for printing !
shading_state_series = np.where(
shading_state_series == None, "Unset", shading_state_series
)
# ------------------------------------------------------------------------
# diffuse_inclined_irradiance_series = np.nan_to_num(
# diffuse_inclined_irradiance_series, nan=0
# )
diffuse_irradiance_reflectivity_coefficient = None
diffuse_inclined_irradiance_reflectivity_factor_series = nan_series
diffuse_inclined_irradiance_before_reflectivity_series = nan_series
if apply_reflectivity_factor:
if abs(surface_tilt - pi) < MINIMAL_DIFFERENCE_THRESHOLD:
surface_tilt -= MINIMAL_DIFFERENCE_THRESHOLD
# Get the reflectivity coefficient
diffuse_irradiance_reflectivity_coefficient = sin(surface_tilt) + (
pi - surface_tilt - sin(surface_tilt)
) / (1 + cos(surface_tilt))
diffuse_irradiance_reflectivity_factor = calculate_reflectivity_factor_for_nondirect_irradiance(
indirect_angular_loss_coefficient=diffuse_irradiance_reflectivity_coefficient,
)
# Get the reflectivity factor from ...
diffuse_inclined_irradiance_reflectivity_factor_series = create_array(
timestamps.shape,
dtype=dtype,
init_method=diffuse_irradiance_reflectivity_factor,
backend=array_backend,
)
# Apply the reflectivity factor
diffuse_inclined_irradiance_series *= (
diffuse_inclined_irradiance_reflectivity_factor_series
)
# avoid copying to save memory and time ... ? ----------------- Is this safe ? -
with errstate(divide="ignore", invalid="ignore"):
# this quantity is exclusively generated for the output dictionary !
diffuse_inclined_irradiance_before_reflectivity_series = where(
diffuse_inclined_irradiance_reflectivity_factor_series != 0,
diffuse_inclined_irradiance_series
/ diffuse_inclined_irradiance_reflectivity_factor_series,
0,
)
# ------------------------------------------------------------------------------
out_of_range, out_of_range_index = identify_values_out_of_range(
series=diffuse_inclined_irradiance_series,
shape=timestamps.shape,
data_model=DiffuseSkyReflectedInclinedIrradiance(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=diffuse_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DiffuseSkyReflectedInclinedIrradiance(
value=diffuse_inclined_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
#
global_horizontal_irradiance=global_horizontal_irradiance_series,
direct_horizontal_irradiance=direct_horizontal_irradiance_series,
diffuse_horizontal_irradiance=diffuse_horizontal_irradiance_series,
extraterrestrial_horizontal_irradiance=extraterrestrial_horizontal_irradiance_series,
#
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
#
reflected=calculate_reflectivity_effect(
irradiance=diffuse_inclined_irradiance_before_reflectivity_series,
reflectivity_factor=diffuse_inclined_irradiance_reflectivity_factor_series,
),
reflectivity_factor=diffuse_inclined_irradiance_reflectivity_factor_series,
reflectivity_coefficient=diffuse_irradiance_reflectivity_coefficient,
value_before_reflectivity=diffuse_inclined_irradiance_before_reflectivity_series,
#
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
term_n=n_series,
kb_ratio=kb_series,
#
elevation=elevation,
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
surface_tilt_threshold=surface_tilt_horizontally_flat_panel_threshold,
#
surface_in_shade=surface_in_shade_series,
solar_incidence=solar_incidence_series,
shading_state=shading_state_series,
solar_altitude=solar_altitude_series,
solar_azimuth=solar_azimuth_series,
solar_azimuth_origin=solar_azimuth_series.origin,
azimuth_difference=azimuth_difference_series,
#
solar_incidence_model=solar_incidence_series.algorithm,
solar_incidence_definition=solar_incidence_series.definition,
shading_states=shading_states,
)
in_shade ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_inclined_irradiance_in_shade | |
calculate_diffuse_inclined_irradiance_in_shade ¶
calculate_diffuse_inclined_irradiance_in_shade(
timestamps,
surface_tilt,
diffuse_inclined_irradiance,
diffuse_sky_irradiance,
diffuse_horizontal_irradiance,
solar_incidence,
surface_in_shade,
shading_states,
shading_state_series,
)
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/in_shade.py
@log_function_call
def calculate_diffuse_inclined_irradiance_in_shade(
timestamps,
surface_tilt,
diffuse_inclined_irradiance, # updated in-place
diffuse_sky_irradiance,
diffuse_horizontal_irradiance,
solar_incidence,
surface_in_shade,
shading_states,
shading_state_series,
):
"""
"""
if ShadingState.in_shade in shading_states:
# ----------------------------------------------------- Review Me
mask_surface_in_shade_series = np.logical_or(
# np.sin(solar_incidence.radians) < 0, # in shade
# will never be True ! when negative incidence angles set to 0 !
solar_incidence.radians < 0, # in shade
# ---------------------------------------------- Review Me ---
# solar_altitude_series.radians >= 0, # yet there is ambient light
surface_in_shade.value # pre-calculated in-shade moments
)
# Is this the _complementary_ incidence angle series ?
# Review Me -----------------------------------------------------
if np.any(mask_surface_in_shade_series):
shading_state_series[mask_surface_in_shade_series] = ShadingState.in_shade.value
logger.info(
f"Shading state series including {ShadingState.in_shade.value} :\n{shading_state_series}",
alt=f"[bold]Shading state[/bold] series including [bold white]{ShadingState.in_shade.value}[/bold white] :\n{shading_state_series}",
)
diffuse_sky_irradiance[mask_surface_in_shade_series] = (
calculate_diffuse_sky_irradiance_series_hofierka(
n_series=full(timestamps.size, TERM_N_IN_SHADE),
surface_tilt=surface_tilt,
)[mask_surface_in_shade_series]
)
diffuse_inclined_irradiance[mask_surface_in_shade_series] = (
diffuse_horizontal_irradiance.value[mask_surface_in_shade_series]
* diffuse_sky_irradiance[mask_surface_in_shade_series]
)
return diffuse_inclined_irradiance
inclined ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_inclined_irradiance_muneer | Calculate the diffuse irradiance incident on a solar surface. |
calculate_diffuse_inclined_irradiance_muneer ¶
calculate_diffuse_inclined_irradiance_muneer(
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex = DatetimeIndex(
[now(tz="UTC")]
),
timezone: ZoneInfo | None = ZoneInfo("UTC"),
global_horizontal_irradiance_series: (
ndarray | None
) = None,
direct_horizontal_irradiance_series: (
ndarray | None
) = None,
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = None,
solar_incidence_series: SolarIncidence | None = None,
surface_in_shade_series: NpNDArray | None = None,
shading_states: List[ShadingState] = [all],
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedInclinedIrradianceFromExternalData
Calculate the diffuse irradiance incident on a solar surface.
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/inclined.py
@log_function_call
@custom_cached
def calculate_diffuse_inclined_irradiance_muneer(
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
surface_tilt_horizontally_flat_panel_threshold: float = SURFACE_TILT_HORIZONTALLY_FLAT_PANEL_THRESHOLD,
timestamps: DatetimeIndex = DatetimeIndex([Timestamp.now(tz='UTC')]),
timezone: ZoneInfo | None = ZoneInfo('UTC'),
global_horizontal_irradiance_series: ndarray | None = None,
direct_horizontal_irradiance_series: ndarray | None = None,
apply_reflectivity_factor: bool = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
solar_altitude_series: SolarAltitude | None = None,
solar_azimuth_series: SolarAzimuth | None = None,
solar_incidence_series: SolarIncidence | None = None,
surface_in_shade_series: NpNDArray | None = None,
shading_states: List[ShadingState] = [ShadingState.all],
solar_constant: float = SOLAR_CONSTANT,
eccentricity_phase_offset: float = ECCENTRICITY_PHASE_OFFSET,
eccentricity_amplitude: float = ECCENTRICITY_CORRECTION_FACTOR,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DiffuseSkyReflectedInclinedIrradianceFromExternalData:
"""Calculate the diffuse irradiance incident on a solar surface."""
# build reusable parameter dictionaries
no_earth_orbit = {
'eccentricity_phase_offset': None,
'eccentricity_amplitude': None,
}
earth_orbit = {
'eccentricity_phase_offset': eccentricity_phase_offset,
'eccentricity_amplitude': eccentricity_amplitude,
}
array_parameters = {
"dtype": dtype,
"array_backend": array_backend,
}
extended_array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"backend": array_backend,
}
if not isinstance(global_horizontal_irradiance_series, ndarray) and not isinstance(
direct_horizontal_irradiance_series, ndarray
):
logger.error(
":information: The global_horizontal_irradiance and/or direct_horizontal_irradiance inputs should be NumPy arrays at this point !",
alt = ":information: [bold red]The [code]global_horizontal_irradiance[/code] and/or [code]direct_horizontal_irradiance[/code] inputs should be [code]NumPy arrays[/code] at this point ![/bold red]",
)
raise ValueError(
":information: The `global_horizontal_irradiance` and/or `direct_horizontal_irradiance` inputs should be NumPy arrays at this point !",
)
logger.info(
":information: Calculating sky-diffuse horizontal irradiance from external time series ...",
alt = ":information: [bold]Calculating[/bold] sky-diffuse horizontal irradiance from external time series ..."
)
diffuse_horizontal_irradiance = calculate_diffuse_horizontal_irradiance_hofierka(
global_horizontal_irradiance_series=global_horizontal_irradiance_series,
direct_horizontal_irradiance_series=direct_horizontal_irradiance_series,
**array_parameters,
verbose=verbose,
log=log,
)
# At this point, the diffuse_horizontal_irradiance_series are either :
# calculated from external time series Or modelled
# Initialise shading_state_series to avoid the "UnboundLocalError"
shading_state_series = create_array(
shape=timestamps.shape,
dtype="object",
init_method="empty",
backend=array_backend,
)
nan_series = create_array(**extended_array_parameters, init_method=np.nan)
unset_series = create_array(**extended_array_parameters, init_method="unset")
# np.full(timestamps.shape, "Unset", "object")
if surface_tilt <= surface_tilt_horizontally_flat_panel_threshold:
diffuse_inclined_irradiance_series = np.copy(
diffuse_horizontal_irradiance.value
)
# to not break the output !
diffuse_sky_irradiance_series = unset_series
n_series = unset_series
kb_series = unset_series
azimuth_difference_series = unset_series
solar_incidence_series = SolarIncidence(
value=unset_series,
algorithm=NOT_AVAILABLE,
definition=NOT_AVAILABLE,
azimuth_origin=NOT_AVAILABLE,
)
extraterrestrial_normal_irradiance_series = ExtraterrestrialNormalIrradiance(
value=unset_series,
unit=NOT_AVAILABLE,
day_angle=unset_series,
solar_constant=None,
**no_earth_orbit,
distance_correction_factor=None,
)
extraterrestrial_horizontal_irradiance_series = ExtraterrestrialHorizontalIrradiance(
value=unset_series,
unit=NOT_AVAILABLE,
day_angle=unset_series,
solar_constant=None,
**no_earth_orbit,
distance_correction_factor=None,
)
else: # tilted (or inclined) surface
# Calculate or get quantities required : ---------------------------- >>> >>> >>>
# 1. to model the diffuse horizontal irradiance [optional]
# 2. to calculate the diffuse sky ... to consider shaded, sunlit and potentially sunlit surfaces
#
extraterrestrial_normal_irradiance_series = (
calculate_extraterrestrial_normal_irradiance_hofierka(
timestamps=timestamps,
solar_constant=solar_constant,
**earth_orbit,
**array_parameters,
verbose=0, # no verbosity here by choice!
log=log,
)
)
extraterrestrial_horizontal_irradiance_series = calculate_extraterrestrial_horizontal_irradiance_series_hofierka(
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
solar_altitude_series=solar_altitude_series,
)
#
# Calculate quantities required : ---------------------------- <<< <<< <<<
with np.errstate(divide="ignore", invalid="ignore"):
kb_series = ( # proportion between direct and extraterrestrial
direct_horizontal_irradiance_series # Array from external data !
/ extraterrestrial_horizontal_irradiance_series.value
)
n_series = calculate_term_n_series_hofierka(
kb_series,
**array_parameters,
verbose=verbose,
)
diffuse_sky_irradiance_series = where(
np.isnan(n_series), # handle NaN cases
0,
calculate_diffuse_sky_irradiance_series_hofierka(
n_series=n_series,
surface_tilt=surface_tilt,
),
)
# prepare size of output array!
diffuse_inclined_irradiance_series = create_array(
**extended_array_parameters, init_method="zeros"
)
diffuse_irradiance = {
'diffuse_inclined_irradiance': diffuse_inclined_irradiance_series,
'diffuse_horizontal_irradiance': diffuse_horizontal_irradiance,
}
# prepare cases : surfaces in-shade, sunlit, potentially sunlit
from pvgisprototype.api.position.models import select_models
shading_states = select_models(
ShadingState, shading_states
) # Using a callback fails!
diffuse_inclined_irradiance_series = calculate_diffuse_inclined_irradiance_in_shade(
# mask_surface_in_shade_series=mask_surface_in_shade_series,
solar_incidence=solar_incidence_series,
surface_in_shade=surface_in_shade_series,
shading_states=shading_states,
shading_state_series=shading_state_series,
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
timestamps=timestamps,
surface_tilt=surface_tilt,
**diffuse_irradiance,
)
diffuse_inclined_irradiance_series = calculate_diffuse_inclined_irradiance_sunlit(
solar_incidence=solar_incidence_series,
shading_states=shading_states,
solar_altitude=solar_altitude_series,
shading_state_series=shading_state_series,
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
kb_series=kb_series,
**diffuse_irradiance,
)
#
azimuth_difference_series = NOT_AVAILABLE # not always required, set to avoid UnboundLocalError!
#
diffuse_inclined_irradiance_series = calculate_diffuse_inclined_irradiance_potentially_sunlit(
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
solar_azimuth=solar_azimuth_series,
shading_states=shading_states,
solar_altitude=solar_altitude_series,
shading_state_series=shading_state_series,
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
kb_series=kb_series,
**diffuse_irradiance,
)
# Replace None with a placeholder -- this is important for printing !
shading_state_series = np.where(
shading_state_series == None, "Unset", shading_state_series
)
# ------------------------------------------------------------------------
# diffuse_inclined_irradiance_series = np.nan_to_num(
# diffuse_inclined_irradiance_series, nan=0
# )
EPSILON = 0.1 #1e-10 # Define a small threshold for comparison
diffuse_irradiance_reflectivity_coefficient = None
diffuse_inclined_irradiance_reflectivity_factor_series = nan_series
diffuse_inclined_irradiance_before_reflectivity_series = nan_series
if apply_reflectivity_factor:
if abs(surface_tilt - pi) < EPSILON:
surface_tilt -= EPSILON
# Get the reflectivity coefficient
diffuse_irradiance_reflectivity_coefficient = sin(surface_tilt) + (
pi - surface_tilt - sin(surface_tilt)
) / (1 + cos(surface_tilt))
diffuse_irradiance_reflectivity_factor = calculate_reflectivity_factor_for_nondirect_irradiance(
indirect_angular_loss_coefficient=diffuse_irradiance_reflectivity_coefficient,
)
# Get the reflectivity factor from ...
diffuse_inclined_irradiance_reflectivity_factor_series = create_array(
**extended_array_parameters,
init_method=diffuse_irradiance_reflectivity_factor,
)
# Apply the reflectivity factor
diffuse_inclined_irradiance_series *= (
diffuse_inclined_irradiance_reflectivity_factor_series
)
# avoid copying to save memory and time ... ? ----------------- Is this safe ? -
with errstate(divide="ignore", invalid="ignore"):
# this quantity is exclusively generated for the output dictionary !
diffuse_inclined_irradiance_before_reflectivity_series = where(
diffuse_inclined_irradiance_reflectivity_factor_series != 0,
diffuse_inclined_irradiance_series
/ diffuse_inclined_irradiance_reflectivity_factor_series,
0,
)
# ------------------------------------------------------------------------------
out_of_range, out_of_range_index = identify_values_out_of_range(
series=diffuse_inclined_irradiance_series,
shape=timestamps.shape,
data_model=DiffuseSkyReflectedInclinedIrradianceFromExternalData(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=diffuse_inclined_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return DiffuseSkyReflectedInclinedIrradianceFromExternalData(
value=diffuse_inclined_irradiance_series,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
#
global_horizontal_irradiance=global_horizontal_irradiance_series,
direct_horizontal_irradiance=direct_horizontal_irradiance_series,
diffuse_horizontal_irradiance=diffuse_horizontal_irradiance,
extraterrestrial_horizontal_irradiance=extraterrestrial_horizontal_irradiance_series,
#
extraterrestrial_normal_irradiance=extraterrestrial_normal_irradiance_series,
#
reflected=calculate_reflectivity_effect(
irradiance=diffuse_inclined_irradiance_before_reflectivity_series,
reflectivity_factor=diffuse_inclined_irradiance_reflectivity_factor_series,
),
reflectivity_factor=diffuse_inclined_irradiance_reflectivity_factor_series,
reflectivity_coefficient=diffuse_irradiance_reflectivity_coefficient,
value_before_reflectivity=diffuse_inclined_irradiance_before_reflectivity_series,
#
diffuse_sky_irradiance=diffuse_sky_irradiance_series,
term_n=n_series,
kb_ratio=kb_series,
#
surface_orientation=surface_orientation,
surface_tilt=surface_tilt,
surface_tilt_threshold=surface_tilt_horizontally_flat_panel_threshold,
#
surface_in_shade=surface_in_shade_series,
solar_incidence=solar_incidence_series,
shading_state=shading_state_series,
solar_altitude=solar_altitude_series,
solar_azimuth=solar_azimuth_series,
solar_azimuth_origin=solar_azimuth_series.origin,
azimuth_difference=azimuth_difference_series,
#
solar_incidence_model=solar_incidence_series.algorithm,
solar_incidence_definition=solar_incidence_series.definition,
shading_states=shading_states,
#
)
potentially_sunlit ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_inclined_irradiance_potentially_sunlit | |
calculate_diffuse_inclined_irradiance_potentially_sunlit ¶
calculate_diffuse_inclined_irradiance_potentially_sunlit(
surface_orientation,
surface_tilt,
solar_azimuth,
solar_altitude,
shading_states,
shading_state_series,
diffuse_inclined_irradiance,
diffuse_sky_irradiance,
kb_series,
diffuse_horizontal_irradiance,
)
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/potentially_sunlit.py
@log_function_call
def calculate_diffuse_inclined_irradiance_potentially_sunlit(
surface_orientation,
surface_tilt,
solar_azimuth,
solar_altitude,
shading_states,
shading_state_series,
diffuse_inclined_irradiance,
diffuse_sky_irradiance,
kb_series,
diffuse_horizontal_irradiance,
):
"""
"""
if ShadingState.potentially_sunlit in shading_states:
mask_potentially_sunlit_surface_series = np.logical_and(
solar_altitude.radians > 0, # sun above horizon
solar_altitude.radians < 0.1, # radians or < 5.7 degrees
shading_state_series == None # operate only on unset elements
)
# else: # if solar altitude < 0.1 : potentially sunlit surface series
if np.any(mask_potentially_sunlit_surface_series):
shading_state_series[mask_potentially_sunlit_surface_series] = ShadingState.potentially_sunlit.value
logger.info(
f"Shading state series including {ShadingState.potentially_sunlit.value} :\n{shading_state_series}",
alt=f"[bold]Shading state[/bold] series including [bold orange]{ShadingState.potentially_sunlit.value}[/bold orange] :\n{shading_state_series}",
)
# requires the solar azimuth
# Normalize the azimuth difference to be within the range -pi to pi
# A0 : solar azimuth _measured from East_ in radians
# ALN : angle between the vertical surface containing the normal to the
# surface and vertical surface passing through the centre of the solar
# disc [rad]
if isinstance(surface_orientation, SurfaceOrientation):
surface_orientation = surface_orientation.value
azimuth_difference_series = (
solar_azimuth.value - surface_orientation
)
azimuth_difference_series = np.arctan2(
np.sin(azimuth_difference_series),
np.cos(azimuth_difference_series),
)
diffuse_inclined_irradiance[
mask_potentially_sunlit_surface_series
] = diffuse_horizontal_irradiance.value[
mask_potentially_sunlit_surface_series
] * (
diffuse_sky_irradiance[mask_potentially_sunlit_surface_series]
* (1 - kb_series[mask_potentially_sunlit_surface_series])
+ kb_series[mask_potentially_sunlit_surface_series]
* sin(surface_tilt)
* np.cos(
azimuth_difference_series[
mask_potentially_sunlit_surface_series
]
)
/ (
0.1
- 0.008
* solar_altitude.radians[
mask_potentially_sunlit_surface_series
]
)
)
return diffuse_inclined_irradiance
sky_irradiance ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_sky_irradiance_series_hofierka | Calculate the diffuse sky irradiance |
calculate_diffuse_sky_irradiance_series_hofierka ¶
calculate_diffuse_sky_irradiance_series_hofierka(
n_series: ndarray,
surface_tilt: float = SURFACE_TILT_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
)
Calculate the diffuse sky irradiance
The diffuse sky irradiance function F(γN) depends on the surface tilt γN (in radians)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
surface_tilt | float | The tilt (also referred to as : inclination or slope) angle of a solar surface | SURFACE_TILT_DEFAULT |
n_series | ndarray | The term N | required |
Returns:
| Type | Description |
|---|---|
Notes | |
----- | |
Internally the function calculates first the dimensionless fraction of the | |
sky dome viewed by a tilted (or inclined) surface `ri(γN)`. | |
The sky-view fraction as defined in Hofierka, 2002 [1]_ is : | (1 + cos(surface_tilt)) / 2 |
which in turn is trigonometrically identical to the definition in Muneer, | |
1990 [2]_ | power(cos(surface_tilt / 2), 2) |
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/sky_irradiance.py
@log_function_call
@custom_cached
def calculate_diffuse_sky_irradiance_series_hofierka(
n_series: ndarray,
surface_tilt: float = SURFACE_TILT_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
):
"""Calculate the diffuse sky irradiance
The diffuse sky irradiance function F(γN) depends on the surface tilt `γN`
(in radians)
Parameters
----------
surface_tilt: float (radians)
The tilt (also referred to as : inclination or slope) angle of a solar
surface
n_series: float
The term N
Returns
-------
Notes
-----
Internally the function calculates first the dimensionless fraction of the
sky dome viewed by a tilted (or inclined) surface `ri(γN)`.
The sky-view fraction as defined in Hofierka, 2002 [1]_ is :
(1 + cos(surface_tilt)) / 2
which in turn is trigonometrically identical to the definition in Muneer,
1990 [2]_
power(cos(surface_tilt / 2), 2)
"""
diffuse_sky_irradiance_series = ((1 + cos(surface_tilt)) / 2) + (
sin(surface_tilt)
- surface_tilt * cos(surface_tilt)
- pi * power((sin(surface_tilt / 2)), 2)
) * n_series
# out_of_range, out_of_range_index = identify_values_out_of_range(
# series=diffuse_sky_irradiance_series,
# shape=n_series.shape,
# # data_model=DiffuseSkyReflectedHorizontalIrradianceFromExternalData(),
# )
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=diffuse_sky_irradiance_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return array(diffuse_sky_irradiance_series, dtype=dtype)
sunlit ¶
Functions:
| Name | Description |
|---|---|
calculate_diffuse_inclined_irradiance_sunlit | |
calculate_diffuse_inclined_irradiance_sunlit ¶
calculate_diffuse_inclined_irradiance_sunlit(
solar_incidence,
shading_states,
solar_altitude,
shading_state_series,
diffuse_sky_irradiance,
kb_series,
diffuse_inclined_irradiance,
diffuse_horizontal_irradiance,
)
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/sunlit.py
@log_function_call
def calculate_diffuse_inclined_irradiance_sunlit(
solar_incidence,
shading_states,
solar_altitude,
shading_state_series,
diffuse_sky_irradiance,
kb_series,
diffuse_inclined_irradiance,
diffuse_horizontal_irradiance,
):
"""
"""
if ShadingState.sunlit in shading_states:
mask_sunlit_surface_series = np.logical_and(
solar_altitude.radians >= 0.1, # or >= 5.7 degrees
shading_state_series == None # operate only on unset elements
)
# else: # sunlit surface and non-overcast sky
# # ----------------------------------------------------------------
# solar_azimuth_series = None ?
# # ----------------------------------------------------------------
if np.any(mask_sunlit_surface_series):
shading_state_series[mask_sunlit_surface_series] = ShadingState.sunlit.value
logger.info(
f"Shading state series including {ShadingState.sunlit.value} :\n{shading_state_series}",
alt=f"[bold]Shading state[/bold] series including [bold yellow]{ShadingState.sunlit.value}[/bold yellow] :\n{shading_state_series}",
)
diffuse_inclined_irradiance[
mask_sunlit_surface_series
] = diffuse_horizontal_irradiance.value[mask_sunlit_surface_series] * (
diffuse_sky_irradiance[mask_sunlit_surface_series]
* (1 - kb_series[mask_sunlit_surface_series])
+ kb_series[mask_sunlit_surface_series]
* np.sin(
solar_incidence.radians[mask_sunlit_surface_series]
) # Should be the _complementary_ incidence angle!
/ np.sin(solar_altitude.radians[mask_sunlit_surface_series])
)
return diffuse_inclined_irradiance
term_n ¶
Functions:
| Name | Description |
|---|---|
calculate_term_n_series_hofierka | Define the N term for a period of time |
calculate_term_n_series_hofierka ¶
calculate_term_n_series_hofierka(
kb_series: ndarray,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> NpNDArray
Define the N term for a period of time
N = 0.00263 − 0.712 × kb − 0.6883 × kb2
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kb_series | ndarray | Direct horizontal to extraterrestrial horizontal irradiance ratio | required |
Returns:
| Name | Type | Description |
|---|---|---|
N | float | The N term |
Notes
Muneer's model treats the shaded and sunlit surfaces separately and further distinguishes between overcast and non-overcast conditions of the sunlit surface. The diffuse sky-reflected irradiance for both surfaces in-shade and sunlit under overcast sky is computed as :
and a sunlit surface under non-overcast sky as :
According to Muneer (1990) the radiance distribution index b (dimensionless)
Source code in pvgisprototype/algorithms/muneer/irradiance/diffuse/term_n.py
@log_function_call
@custom_cached
def calculate_term_n_series_hofierka(
kb_series: ndarray,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> NpNDArray:
"""Define the N term for a period of time
N = 0.00263 − 0.712 × kb − 0.6883 × kb2
Parameters
----------
kb_series: float
Direct horizontal to extraterrestrial horizontal irradiance ratio
Returns
-------
N: float
The N term
Notes
-----
Muneer's model treats the shaded and sunlit surfaces separately and further
distinguishes between overcast and non-overcast conditions of the sunlit
surface. The diffuse sky-reflected irradiance for both surfaces in-shade and
sunlit under _overcast_ sky is computed as :
and a sunlit surface under non-overcast sky as :
According to Muneer (1990) the radiance distribution index `b` (dimensionless)
"""
kb_series = np.array(kb_series, dtype=dtype)
term_n_series = (
0.00263 - (0.712 * kb_series) - (0.6883 * np.power(kb_series, 2, dtype=dtype))
)
log_data_fingerprint(
data=term_n_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
return term_n_series
noaa ¶
Modules:
| Name | Description |
|---|---|
equation_of_time | The Equation of Time based on the General Solar Position Calculations provided |
event_hour_angle | |
event_time | |
events | This module contains days within the year that have some solar significance. |
fractional_year | |
local_time | |
parameter_models | |
solar_altitude | |
solar_azimuth | |
solar_declination | |
solar_hour_angle | |
solar_time | The true solar time based on NOAA's General Solar Position Calculations. |
solar_zenith | |
time_offset | The time offset based on NOAA's General Solar Position Calculations. |
equation_of_time ¶
The Equation of Time based on the General Solar Position Calculations provided by the NOAA Global Monitoring Division.
See also: https://unpkg.com/solar-calculator@0.1.0/index.js
Functions:
| Name | Description |
|---|---|
calculate_equation_of_time_series_noaa | Calculate the Equation of Time for a time series in minutes. |
calculate_equation_of_time_series_noaa ¶
calculate_equation_of_time_series_noaa(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> EquationOfTime
Calculate the Equation of Time for a time series in minutes.
The Equation of Time is the difference between the apparent solar time and the mean solar time.
Returns:
| Name | Type | Description |
|---|---|---|
equation_of_time_series | numeric | Difference in time between solar time and mean solar time in minutes. |
Source code in pvgisprototype/algorithms/noaa/equation_of_time.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateEquationOfTimeTimeSeriesNOAAInput)
def calculate_equation_of_time_series_noaa(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> EquationOfTime:
"""Calculate the Equation of Time for a time series in minutes.
The Equation of Time is the difference between the apparent solar time and
the mean solar time.
Returns
-------
equation_of_time_series : numeric
Difference in time between solar time and mean solar time in minutes.
"""
fractional_year_series = calculate_fractional_year_series_noaa(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
equation_of_time_series = 229.18 * (
0.000075
+ 0.001868 * np.cos(fractional_year_series.radians)
- 0.032077 * np.sin(fractional_year_series.radians)
- 0.014615 * np.cos(2 * fractional_year_series.radians)
- 0.040849 * np.sin(2 * fractional_year_series.radians)
)
if validate_output:
if not np.all(
(EquationOfTime().min_minutes <= equation_of_time_series)
& (equation_of_time_series <= EquationOfTime().max_minutes)
):
raise ValueError(
"The equation of time must be within the range [{EquationOfTime().min_minutes}, {EquationOfTime().max_minutes()}] minutes for all timestamps."
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=equation_of_time_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return EquationOfTime(
value=equation_of_time_series,
unit=MINUTES,
position_algorithm=fractional_year_series.position_algorithm,
timing_algorithm=SolarTimeModel.noaa,
)
event_hour_angle ¶
Functions:
| Name | Description |
|---|---|
calculate_event_hour_angle_series_noaa | Calculates the event hour angle using the NOAA method. |
calculate_event_hour_angle_series_noaa ¶
calculate_event_hour_angle_series_noaa(
latitude: Latitude,
timestamps: DatetimeIndex,
unrefracted_solar_zenith: UnrefractedSolarZenith,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> EventHourAngle
Calculates the event hour angle using the NOAA method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
latitude | Latitude | The geographic latitude for which to calculate the event hour angle. | required |
timestamp | datetime | The date and time for which to calculate the event hour angle. | required |
unrefracted_solar_zenith | UnrefractedSolarZenith | The zenith of the sun, adjusted for atmospheric refraction. Defaults to 1.5853349194640094 radians, which corresponds to 90.833 degrees. This is the zenith at sunrise or sunset, adjusted for the approximate correction for atmospheric refraction at those times, and the size of the solar disk. | required |
angle_units | str | The unit in which the angles are input. Defaults to 'radians'. | required |
Returns:
| Type | Description |
|---|---|
EventHourAngle | PVGIS native data structure carrying the calculated event hour angle along with relevant metadata. |
Notes
The function implements NOAA calculations for the solar declination and the event hour angle. The solar declination is calculated first in radians, followed by the event hour angle in radians.
Commented out: If the output units are 'degrees', the function will convert the calculated event hour angle from radians to degrees.
Source code in pvgisprototype/algorithms/noaa/event_hour_angle.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateEventHourAngleTimeSeriesNOAAInput)
def calculate_event_hour_angle_series_noaa(
latitude: Latitude,
timestamps: DatetimeIndex,
unrefracted_solar_zenith: UnrefractedSolarZenith,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> EventHourAngle:
"""
Calculates the event hour angle using the NOAA method.
Parameters
----------
latitude : Latitude
The geographic latitude for which to calculate the event hour angle.
timestamp : datetime
The date and time for which to calculate the event hour angle.
unrefracted_solar_zenith : UnrefractedSolarZenith, optional
The zenith of the sun, adjusted for atmospheric refraction. Defaults to
1.5853349194640094 radians, which corresponds to 90.833 degrees. This
is the zenith at sunrise or sunset, adjusted for the approximate
correction for atmospheric refraction at those times, and the size of
the solar disk.
angle_units : str, optional
The unit in which the angles are input. Defaults to 'radians'.
Returns
-------
EventHourAngle
PVGIS native data structure carrying the calculated event hour angle
along with relevant metadata.
Notes
-----
The function implements NOAA calculations for the solar declination and
the event hour angle. The solar declination is calculated first in radians,
followed by the event hour angle in radians.
Commented out: If the output units are 'degrees', the function
will convert the calculated event hour angle from radians to degrees.
"""
solar_declination_series = calculate_solar_declination_series_noaa(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
) # radians
cosine_event_hour_angle_series = np.cos(unrefracted_solar_zenith.radians) / (
np.cos(latitude.radians) * np.cos(solar_declination_series.radians)
) - np.tan(latitude.radians) * np.tan(solar_declination_series.radians)
event_hour_angle_series = np.arccos(
np.clip(cosine_event_hour_angle_series, -1, 1)
) # radians
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=event_hour_angle_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return EventHourAngle(
value=event_hour_angle_series,
unit=RADIANS,
timing_algorithm=SolarTimeModel.noaa,
)
event_time ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_event_time_in_minutes | Calculate the event time in minutes based on the event type. |
calculate_solar_event_time_series_noaa | Calculate the time of solar events like sunrise, noon, or sunset. |
match_event_times_to_timestamps | |
calculate_solar_event_time_in_minutes ¶
calculate_solar_event_time_in_minutes(
longitude: Longitude,
event_hour_angle: float,
equation_of_time: float,
timezone_offset_hours_utc: float,
event_type: str,
) -> float
Calculate the event time in minutes based on the event type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | Longitude | The longitude in radians. | required |
event_hour_angle | float | The event hour angle in minutes. | required |
equation_of_time | float | The equation of time in minutes. | required |
timezone_offset_hours_utc | float | The timezone offset in hours. | required |
event_type | str | The type of solar event ('sunrise', 'noon', 'sunset'). | required |
Returns:
| Type | Description |
|---|---|
float | The calculated event time in minutes. |
Source code in pvgisprototype/algorithms/noaa/event_time.py
def calculate_solar_event_time_in_minutes(
longitude: Longitude,
event_hour_angle: float,
equation_of_time: float,
timezone_offset_hours_utc: float,
event_type: str,
) -> float:
"""
Calculate the event time in minutes based on the event type.
Parameters
----------
longitude : Longitude
The longitude in radians.
event_hour_angle : float
The event hour angle in minutes.
equation_of_time : float
The equation of time in minutes.
timezone_offset_hours_utc : float
The timezone offset in hours.
event_type : str
The type of solar event ('sunrise', 'noon', 'sunset').
Returns
-------
float
The calculated event time in minutes.
"""
# Calculate the base time
base = 720 - equation_of_time.minutes + timezone_offset_hours_utc * 60
longitude_minutes = longitude.as_minutes
event_hour_angle_minutes = event_hour_angle.as_minutes
# Define event calculations
event_calculations = {
SolarEvent.astronomical_twilight.name: lambda: base - (longitude_minutes + event_hour_angle_minutes + 4 * 18),
SolarEvent.nautical_twilight.name: lambda: base - (longitude_minutes + event_hour_angle_minutes + 4 * 12),
SolarEvent.civil_twilight.name: lambda: base - (longitude_minutes + event_hour_angle_minutes + 4 * 6),
SolarEvent.sunrise.name: lambda: base - (longitude_minutes + event_hour_angle_minutes),
SolarEvent.noon.name: lambda: base - longitude_minutes,
SolarEvent.sunset.name: lambda: base - (longitude_minutes - event_hour_angle_minutes),
SolarEvent.daylength.name: lambda: (
base - (longitude_minutes - event_hour_angle_minutes) -
(base - (longitude_minutes + event_hour_angle_minutes))
),
}
# Calculate and return the event time
if event_type in event_calculations:
return event_calculations[event_type]()
else:
# raise ValueError(f"Unknown event type: {event_type}")
logger.warning(
f"Calculation for the {event_type} yet not implemented!",
alt= f"Calculation for the [code]{event_type}[/code] yet [bold]not implemented[/bold]!",
)
return NaT
calculate_solar_event_time_series_noaa ¶
calculate_solar_event_time_series_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
event: List[SolarEvent | None] = [None],
unrefracted_solar_zenith: UnrefractedSolarZenith = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> EventTime
Calculate the time of solar events like sunrise, noon, or sunset.
For sunrise and sunset, the zenith is set to 90.833 degrees (the approximate correction for atmospheric refraction and the size of the solar disk).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
latitude | Latitude | The latitude in radians. | required |
longitude | Longitude | The longitude in radians. | required |
timestamps | DatetimeIndex | The date to calculate the event for. | required |
timezone | ZoneInfo | The timezone information. | required |
unrefracted_solar_zenith | UnrefractedSolarZenith | The zenith of the sun, adjusted for atmospheric refraction. Defaults to 1.5853349194640094 radians, which corresponds to 90.833 degrees. This is the zenith at sunrise or sunset, adjusted for the approximate correction for atmospheric refraction at those times, and the size of the solar disk. | UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT |
event | List[Optional[SolarEvent]] | A list of solar events to calculate the hour angle for, i.e. 'noon', 'sunrise', or 'sunset'. | [None] |
dtype | str | Data type for calculations. Default is DATA_TYPE_DEFAULT. | DATA_TYPE_DEFAULT |
array_backend | str | Backend for array operations. Default is ARRAY_BACKEND_DEFAULT. | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level for logging. Default is VERBOSE_LEVEL_DEFAULT. | VERBOSE_LEVEL_DEFAULT |
log | int | Log level for logging. Default is LOG_LEVEL_DEFAULT. | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
EventTime | The calculated times of the requested solar events as a DatetimeIndex. |
Notes
- All angles are in radians.
- The calculation
(1440 / (2 * pi)) * value_in_radiansmaps a 'value in radians' from a range of [0, 2 * pi] which is a full circle to a range of [0, 1440] which is a full day in minutes.
Source code in pvgisprototype/algorithms/noaa/event_time.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateEventTimeTimeSeriesNOAAInput)
def calculate_solar_event_time_series_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
event: List[SolarEvent | None] = [None],
unrefracted_solar_zenith: UnrefractedSolarZenith = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
# adjust_for_atmospheric_refraction: bool = False,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> EventTime:
"""Calculate the time of solar events like sunrise, noon, or sunset.
For sunrise and sunset, the zenith is set to 90.833 degrees (the
approximate correction for atmospheric refraction and the size of the solar
disk).
Parameters
----------
latitude : Latitude
The latitude in radians.
longitude : Longitude
The longitude in radians.
timestamps : DatetimeIndex
The date to calculate the event for.
timezone : ZoneInfo
The timezone information.
unrefracted_solar_zenith : UnrefractedSolarZenith, optional
The zenith of the sun, adjusted for atmospheric refraction. Defaults to
1.5853349194640094 radians, which corresponds to 90.833 degrees. This
is the zenith at sunrise or sunset, adjusted for the approximate
correction for atmospheric refraction at those times, and the size of
the solar disk.
event : List[Optional[SolarEvent]], optional
A list of solar events to calculate the hour angle for, i.e. 'noon', 'sunrise', or 'sunset'.
dtype : str, optional
Data type for calculations. Default is DATA_TYPE_DEFAULT.
array_backend : str, optional
Backend for array operations. Default is ARRAY_BACKEND_DEFAULT.
verbose : int, optional
Verbosity level for logging. Default is VERBOSE_LEVEL_DEFAULT.
log : int, optional
Log level for logging. Default is LOG_LEVEL_DEFAULT.
Returns
-------
EventTime
The calculated times of the requested solar events as a DatetimeIndex.
Notes
-----
- All angles are in radians.
- The calculation `(1440 / (2 * pi)) * value_in_radians` maps a 'value in
radians' from a range of [0, 2 * pi] which is a full circle to a range of
[0, 1440] which is a full day in minutes.
"""
if event == [SolarEvent.none]:
return EventTime(
value=None,
event=None,
)
# Create an array of NaTs based on unique days from timestamps
unique_days = timestamps.normalize().unique()
event_hour_angle_series = calculate_event_hour_angle_series_noaa(
latitude=latitude,
timestamps=unique_days,
unrefracted_solar_zenith=unrefracted_solar_zenith,
)
equation_of_time_series = calculate_equation_of_time_series_noaa(
timestamps=unique_days,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
)
timezone_offset_timedelta = timezone.utcoffset(None)
timezone_offset_hours_utc = timezone_offset_timedelta.total_seconds() / 3600
from pvgisprototype.api.position.models import select_models
requested_solar_events = select_models(
SolarEvent, event
) # Using a callback fails!
# Vectorized calculation of event times
solar_events_time_series_in_minutes = {
solar_event: calculate_solar_event_time_in_minutes(
longitude,
event_hour_angle_series,
equation_of_time_series,
timezone_offset_hours_utc,
solar_event.name,
)
for solar_event in requested_solar_events if solar_event in SolarEvent
}
solar_event_series = dict()
for solar_event in requested_solar_events:
# Check if the event has calculated times
if solar_event in solar_events_time_series_in_minutes:
# Get the event times in minutes
solar_event_series[solar_event] = DatetimeIndex(
[
Timestamp(ts.date()) + timedelta(minutes=float(et))
for ts, et in zip(
unique_days, solar_events_time_series_in_minutes[solar_event]
)
]
)
else:
logger.error(f"Event {event} not found in event_time_series_in_minutes.")
event_types, event_timestamps = match_event_times_to_timestamps(
solar_event_series=solar_event_series,
timestamps=timestamps,
array_backend=array_backend,
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_event_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return EventTime(
value=event_timestamps,
event_type=event_types,
hour_angle=event_hour_angle_series,
equation_of_time=equation_of_time_series,
)
match_event_times_to_timestamps ¶
match_event_times_to_timestamps(
solar_event_series: dict,
timestamps: DatetimeIndex,
array_backend: str,
)
Source code in pvgisprototype/algorithms/noaa/event_time.py
def match_event_times_to_timestamps(
solar_event_series: dict,
timestamps: DatetimeIndex,
array_backend: str,
):
"""
"""
frequency_string = timestamps.freqstr
try:
# Define matching threshold dynamically based on inferred frequency
if 'W-' in str(frequency_string):
frequency_string = '1' + str(frequency_string).split('-')[0] # 'W'
else:
frequency_string = timestamps.freq
frequency_threshold = Timedelta(frequency_string)
except:
raise ValueError("Unable to infer frequency from timestamps.")
event_timestamps = numpy.full(
shape=timestamps.shape,
fill_value=numpy.datetime64('NaT'),
dtype='datetime64[ns]',
)
event_types = create_array(
timestamps.shape, dtype="object", init_method="empty", backend=array_backend
)
naive_timestamps = timestamps.tz_localize(None)
for event_type, event_times in solar_event_series.items():
# Find indices of closest matching timestamps
event_indexer = naive_timestamps.get_indexer(event_times, method='nearest')
# However, assign event times to indices only if within the frequency threshold
for idx, event_time in zip(event_indexer, event_times):
if idx >= 0: # Valid index
closest_timestamp = naive_timestamps[idx]
if abs(event_time - closest_timestamp) <= frequency_threshold:
event_timestamps[idx] = event_time.to_numpy()
event_types[idx] = event_type
return event_types, event_timestamps
events ¶
This module contains days within the year that have some solar significance.
The SIGNIFICANT_DAYS list includes key dates such as solstices, equinoxes, and points of perihelion and aphelion, among others. These dates are significant in understanding the Earth's orbit around the Sun and its effects on solar irradiance.
Each date is represented as a tuple, where the first element is the day of the year, and the second element is an event or description.
Constants: SIGNIFICANT_DAYS (list): A list of tuples representing significant dates. Each tuple contains a day of the year and a description of the significant event on that day. Source : Significant Days of the Year, https://andrewmarsh.com/articles/2019/significant-dates/
fractional_year ¶
Functions:
| Name | Description |
|---|---|
calculate_fractional_year_series_noaa | Calculate the fractional year for a time series. |
calculate_fractional_year_series_noaa ¶
calculate_fractional_year_series_noaa(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> FractionalYear
Calculate the fractional year for a time series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamps | DatetimeIndex | A Pandas DatetimeIndex representing the timestamps. | required |
dtype | str | The data type for the calculations (the default is 'float32'). | DATA_TYPE_DEFAULT |
array_backend | str | The backend used for calculations (the default is 'NUMPY'). | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level | VERBOSE_LEVEL_DEFAULT |
log | int | Log level | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
FractionalYear | A FractionalYear object containing the calculated fractional year series. |
Raises:
| Type | Description |
|---|---|
ValueError | If any calculated fractional year value is outside the expected range. |
Examples:
>>> timestamps = pd.date_range(start='2020-01-01', end='2020-12-31', freq='D')
>>> fractional_year_series = calculate_fractional_year_series_noaa(timestamps)
>>> print(fractional_year_series)
Notes
The function calculates the fractional year considering leap years and converts the timestamps into fractional values considering their position within the year.
See also
The default data type (dtype) and backend (array_backend) for arrays are set in constants.py via the global variables DATA_TYPE_DEFAULT and ARRAY_BACKEND_DEFAULT.
Source code in pvgisprototype/algorithms/noaa/fractional_year.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateFractionalYearTimeSeriesNOAAInput)
def calculate_fractional_year_series_noaa(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT
) -> FractionalYear:
"""Calculate the fractional year for a time series.
Parameters
----------
timestamps : DatetimeIndex
A Pandas DatetimeIndex representing the timestamps.
dtype : str, optional
The data type for the calculations (the default is 'float32').
array_backend : str, optional
The backend used for calculations (the default is 'NUMPY').
verbose: int
Verbosity level
log: int
Log level
Returns
-------
FractionalYear
A FractionalYear object containing the calculated fractional year
series.
Raises
------
ValueError
If any calculated fractional year value is outside the expected range.
Examples
--------
>>> timestamps = pd.date_range(start='2020-01-01', end='2020-12-31', freq='D')
>>> fractional_year_series = calculate_fractional_year_series_noaa(timestamps)
>>> print(fractional_year_series)
Notes
-----
The function calculates the fractional year considering leap years and
converts the timestamps into fractional values considering their position
within the year.
See also
--------
The default data type (`dtype`) and backend (`array_backend`) for arrays
are set in `constants.py` via the global variables `DATA_TYPE_DEFAULT` and
`ARRAY_BACKEND_DEFAULT`.
"""
days_of_year = timestamps.dayofyear
hours = timestamps.hour
days_in_years = get_days_in_years(timestamps.year)
array_parameters = {
"shape": timestamps.shape,
"dtype": dtype,
"init_method": "zeros",
"backend": array_backend,
} # Borrow shape from timestamps
fractional_year_series = create_array(**array_parameters)
fractional_year_series = np.array(
2 * np.pi / days_in_years * (days_of_year - 1 + (hours - 12) / 24),
dtype=dtype,
)
# Is this "restriction" correct ?
fractional_year_series[fractional_year_series < 0] = 0
if validate_output:
if not np.all(
(FractionalYear().min_radians <= fractional_year_series)
& (fractional_year_series <= FractionalYear().max_radians)
):
index_of_out_of_range_values = np.where(
(fractional_year_series < FractionalYear().min_radians)
| (fractional_year_series > FractionalYear().max_radians)
)
out_of_range_values = fractional_year_series[index_of_out_of_range_values]
# Report values in "human readable" degrees
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{FractionalYear().min_radians}, {FractionalYear().max_radians}] radians"
f" in [code]fractional_year_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=fractional_year_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return FractionalYear(
value=fractional_year_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.noaa,
)
local_time ¶
Functions:
| Name | Description |
|---|---|
calculate_local_solar_time_noaa | Calculate the Local Solar Time (LST) based on the sun's position. |
calculate_local_solar_time_noaa ¶
calculate_local_solar_time_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
event: SolarEvent | None = noon,
unrefracted_solar_zenith: UnrefractedSolarZenith = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DatetimeIndex
Calculate the Local Solar Time (LST) based on the sun's position.
This function computes the local solar time for a series of timestamps, considering solar noon and corrections for the Equation of Time (ET) and longitude. Local solar time reflects the actual position of the sun in the sky, which may differ from local standard time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | Longitude | Longitude of the location in radians. | required |
latitude | Latitude | Latitude of the location in radians. | required |
timestamps | DatetimeIndex | Timestamps for which to calculate the local solar time. | required |
timezone | ZoneInfo | Timezone information for the location. | required |
unrefracted_solar_zenith | UnrefractedSolarZenith | The zenith of the sun, adjusted for atmospheric refraction. Defaults to 1.5853349194640094 radians, which corresponds to 90.833 degrees. This is the zenith at sunrise or sunset, adjusted for the approximate correction for atmospheric refraction at those times, and the size of the solar disk. | UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT |
dtype | str | Data type for the output (default is DATA_TYPE_DEFAULT). | DATA_TYPE_DEFAULT |
array_backend | str | Backend for array operations (default is ARRAY_BACKEND_DEFAULT). | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level for debugging (default is VERBOSE_LEVEL_DEFAULT). | VERBOSE_LEVEL_DEFAULT |
log | int | Logging level (default is LOG_LEVEL_DEFAULT). | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
DatetimeIndex | A series of local solar times corresponding to the input timestamps. |
Notes
In solar energy calculations, the Apparent Solar Time (AST) is based on the
apparent angular motion of the sun across the sky, expressing the time of day. The moment when the sun crosses the meridian of the observer is known as local solar noon. This does not usually coincide with 12:00 PM on the clock.
Local Solar Time (LST) is calculated based on the Apparent Solar Time (AST), which is determined by the sun's position. The general equation for converting between Local Solar Time (LST) and Apparent Solar Time (AST) is:
AST = LST + ET ± 4 * (SL - LL) - DS
Where: - AST: Apparent Solar Time - LST: Local Standard Time - ET: Equation of Time - SL: Standard Longitude for the time zone - LL: Local Longitude - DS: Daylight Saving Time adjustment. Working with UTC, DS is removed from the equation.
Thus, the LST is given by :
LST = AST - ET ± 4 * (SL - LL)
For noon, AST = 12 thus :
LST = 12 - ET ± 4 * (SL - LL)
Definitions:
- Local Solar Time (LST): Time based on the sun's position at a specific location.
- Apparent Solar Time (AST): Time measured by the sun's position, specifically at solar noon.
- Equation of Time (ET): Difference between apparent solar time and mean solar time.
- Standard Longitude: Longitude that defines the center of a time zone.
- Local Longitude: Specific longitude of a location, used for corrections.
- Daylight Saving Time (DS): Adjustment of the clock to extend evening daylight.
This function is useful for solar energy calculations and other applications where the actual solar time is relevant.
Source code in pvgisprototype/algorithms/noaa/local_time.py
@validate_with_pydantic(CalculateLocalSolarTimeNOAAInput)
def calculate_local_solar_time_noaa(
longitude: Longitude, # radians
latitude: Latitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo,
event: SolarEvent | None = SolarEvent.noon,
unrefracted_solar_zenith: UnrefractedSolarZenith = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT, # radians
# adjust_for_atmospheric_refraction: bool = False,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> DatetimeIndex:
"""
Calculate the Local Solar Time (LST) based on the sun's position.
This function computes the local solar time for a series of timestamps,
considering solar noon and corrections for the Equation of Time (ET)
and longitude. Local solar time reflects the actual position of the sun
in the sky, which may differ from local standard time.
Parameters
----------
longitude : Longitude
Longitude of the location in radians.
latitude : Latitude
Latitude of the location in radians.
timestamps : DatetimeIndex
Timestamps for which to calculate the local solar time.
timezone : ZoneInfo
Timezone information for the location.
unrefracted_solar_zenith : UnrefractedSolarZenith, optional
The zenith of the sun, adjusted for atmospheric refraction. Defaults to
1.5853349194640094 radians, which corresponds to 90.833 degrees. This
is the zenith at sunrise or sunset, adjusted for the approximate
correction for atmospheric refraction at those times, and the size of
the solar disk.
# adjust_for_atmospheric_refraction : bool, optional
# Whether to apply atmospheric refraction corrections (default is False).
dtype : str, optional
Data type for the output (default is DATA_TYPE_DEFAULT).
array_backend : str, optional
Backend for array operations (default is ARRAY_BACKEND_DEFAULT).
verbose : int, optional
Verbosity level for debugging (default is VERBOSE_LEVEL_DEFAULT).
log : int, optional
Logging level (default is LOG_LEVEL_DEFAULT).
Returns
-------
DatetimeIndex
A series of local solar times corresponding to the input timestamps.
Notes
-----
In solar energy calculations, the Apparent Solar Time (AST) is based on the
apparent angular motion of the sun across the sky, expressing the time of day.
The moment when the sun crosses the meridian of the observer is known as
local solar noon. This does not usually coincide with 12:00 PM on the clock.
Local Solar Time (LST) is calculated based on the Apparent Solar Time (AST),
which is determined by the sun's position. The general equation for converting
between Local Solar Time (LST) and Apparent Solar Time (AST) is:
AST = LST + ET ± 4 * (SL - LL) - DS
Where:
- AST: Apparent Solar Time
- LST: Local Standard Time
- ET: Equation of Time
- SL: Standard Longitude for the time zone
- LL: Local Longitude
- DS: Daylight Saving Time adjustment. Working with UTC, DS is removed from the equation.
Thus, the LST is given by :
LST = AST - ET ± 4 * (SL - LL)
For noon, AST = 12 thus :
LST = 12 - ET ± 4 * (SL - LL)
Definitions:
- **Local Solar Time (LST)**: Time based on the sun's position at a specific location.
- **Apparent Solar Time (AST)**: Time measured by the sun's position, specifically at solar noon.
- **Equation of Time (ET)**: Difference between apparent solar time and mean solar time.
- **Standard Longitude**: Longitude that defines the center of a time zone.
- **Local Longitude**: Specific longitude of a location, used for corrections.
- **Daylight Saving Time (DS)**: Adjustment of the clock to extend evening daylight.
This function is useful for solar energy calculations and other applications
where the actual solar time is relevant.
"""
# Calculate solar noon for the given timestamps
solar_noon_series = calculate_solar_event_time_series_noaa(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
event=event.name,
unrefracted_solar_zenith=unrefracted_solar_zenith,
# adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
)
# Calculate the time difference from solar noon
local_solar_time_delta = np.where(
timestamps < solar_noon_series,
timestamps - (solar_noon_series - timedelta(days=1)), # Previous solar noon
timestamps - solar_noon_series, # Current solar noon
)
# Convert time difference to seconds and add to timestamps
total_seconds = int(local_solar_time_delta.total_seconds())
local_solar_time_series = timestamps + timedelta(seconds=total_seconds)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
return local_solar_time_series
parameter_models ¶
Classes:
| Name | Description |
|---|---|
AngleInRadiansOutputUnitsModel | The angle in radians output units argument is passed along with the |
AngleInRadiansOutputUnitsModel ¶
Bases: BaseModel
The angle in radians output units argument is passed along with the returned value. This is not a real test. Hopefully, and however, it helps for clarity and understanding of what the function should return.
solar_altitude ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_altitude_series_noaa | Calculate the solar altitude angle for a location over a time series |
calculate_solar_altitude_series_noaa ¶
calculate_solar_altitude_series_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
adjust_for_atmospheric_refraction: bool = True,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAltitude
Calculate the solar altitude angle for a location over a time series
Source code in pvgisprototype/algorithms/noaa/solar_altitude.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSolarAltitudeTimeSeriesNOAAInput)
def calculate_solar_altitude_series_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
adjust_for_atmospheric_refraction: bool = True,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAltitude:
"""Calculate the solar altitude angle for a location over a time series"""
solar_zenith_series = calculate_solar_zenith_series_noaa(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
solar_altitude_series = np.pi / 2 - solar_zenith_series.radians
out_of_range, out_of_range_index = identify_values_out_of_range_x(
series=solar_altitude_series,
shape=timestamps.shape,
data_model=SolarAltitude(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_altitude_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAltitude(
value=solar_altitude_series,
unit=RADIANS,
out_of_range=out_of_range,
out_of_range_index=out_of_range_index,
solar_positioning_algorithm=solar_zenith_series.solar_positioning_algorithm,
solar_timing_algorithm=solar_zenith_series.solar_timing_algorithm,
adjusted_for_atmospheric_refraction=solar_zenith_series.adjusted_for_atmospheric_refraction,
)
solar_azimuth ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_azimuth_series_noaa | Calculate the solar azimuth angle (θ) for a time series at a specific |
calculate_solar_azimuth_series_noaa ¶
calculate_solar_azimuth_series_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo | None,
adjust_for_atmospheric_refraction: bool = True,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAzimuth
Calculate the solar azimuth angle (θ) for a time series at a specific geographic latitude and longitude.
Calculate the solar azimuth angle (θ) for a time series at a specific geographic latitude and longitude, by default correcting the solar zenith angle for atmospheric refraction, as described in the following steps:
1. Calculate the solar declination, solar hour angle, and solar zenith
using NOAA's General Solar Position Calculations.
2. Calculate the solar azimuth angle using the equation reported in
Wikipedia's article "Solar azimuth angle" adjusting for the time of
day -- see also Notes :
- Morning (solar hour angle < 0): azimuth derived directly from the
arccosine function.
- Afternoon (solar hour angle > 0): azimuth adjusted to range in
[180°, 360°] by subtracting it from 360°.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | float | Longitude of the location in radians. | required |
latitude | float | Latitude of the location in radians. | required |
timestamps | DatetimeIndex | Times for which the solar azimuth will be calculated. | required |
timezone | ZoneInfo | Timezone of the location. | required |
adjust_for_atmospheric_refraction | bool | Whether to correct the solar zenith angle for atmospheric refraction. | True |
dtype | str | Data type for the calculations. | DATA_TYPE_DEFAULT |
array_backend | str | Backend array library to use. | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level of the function. | VERBOSE_LEVEL_DEFAULT |
log | int | Log level for the function. | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
SolarAzimuth | A custom data class that hold a NumPy NDArray of calculated solar azimuth angles in radians, a method to convert the angles to degrees and other metadata. |
Notes
From .. Excel sheet :
=IF(AC2>0, MOD( DEGREES(ACOS(((SIN(RADIANS(\(B\)3))*COS(RADIANS(AD2)))-SIN(RADIANS(T2)))/(COS(RADIANS(\(B\)3))*SIN(RADIANS(AD2)))))+180, 360), MOD( 540-DEGREES(ACOS(((SIN(RADIANS(\(B\)3))*COS(RADIANS(AD2)))-SIN(RADIANS(T2)))/(COS(RADIANS(\(B\)3))*SIN(RADIANS(AD2))))), 360) )
numerator = (sin(latitude.radians) * cos(solar_zenith.radians)) - sin(solar_declination.radians) denominator = (cos(latitude.radians) * sin(solar_zenith.radians)) cosine_solar_azimuth = numerator / denominator
if solar_hour_angle > 0: (pi + arccos(cosine_solar_azimuth)) % 2*pi
else: (3*pi - arccos(cosine_solar_azimuth)) % 2*pi
Two important notes on the calculation of the solar azimuth angle :
-
The equation implemented here follows upon the relevant Wikipedia article for the "Solar azimuth angle" [1]_.
-
The angle derived fom the arccosine function requires an adjustment to correctly represent both morning and afternoon solar azimuth angles.
-
The equation given in NOAA's General Solar Position Calculations [0]_ is
sin(latitude) * cos(solar_zenith) - sin(solar_declination)cos(180 - θ) = - ---------------------------------------------------------- cos(latitude) * sin(solar_zenith)
or after converting cos(180 - θ) to - cos(θ) :
sin(latitude) * cos(solar_zenith) - sin(solar_declination)- cos(θ) = - ------------------------------------------------------------ cos(latitude) * sin(solar_zenith)
or :
sin(latitude) * cos(solar_zenith) - sin(solar_declination)cos(θ) = ---------------------------------------------------------- cos(latitude) * sin(solar_zenith)
and finally :
sin(latitude) * cos(solar_zenith) - sin(solar_declination)θ = arccos( -------------------------------------------------------------- ) cos(latitude) * sin(solar_zenith)
where θ is the wanted solar azimuth angle.
However, comparing this equation with the almost identical equation reported in Wikipedia's relevant article and subsection titled "Conventional Trigonometric Formulas" [1]_, there seems to be a difference of a minus sign :
sin(declination) - cos(zenith) * sin(latitude)φs = arccos( ---------------------------------------------- ) sin(zenith) * cos(latitude)
where φs is the wanted solar azimuth angle.
A cross-comparison of the solar azimuth angle derived by the equation reported in Wikipedia's article [1]_ and the libraries pvlib [2], Skyfield [3] and suncalc [4]_, show a high agreement across all of them.
-
Adjusting the solar azimuth based on the time of day
Given that arccosine ranges in [0, π] or else in [0°, 180°], the raw calculated solar azimuth angle will likewise range in [0, π]. This necessitates an adjustment based on the time of day.
-
Morning (solar hour angle < 0): the azimuth angle is correctly derived directly from the arccosine function representing angles from the North clockwise to the South [0°, 180°].
-
Afternoon (solar hour angle > 0): the azimuth angle needs to be adjusted in order to correctly represent angles going further from the South to the West [180°, 360°]. This is achieved by subtracting the azimuth from 360°.
-
References
.. [0] https://gml.noaa.gov/grad/solcalc/solareqns.PDF
.. [1] https://en.wikipedia.org/wiki/Solar_azimuth_angle#Conventional_Trigonometric_Formulas
.. [2] https://github.com/pvlib/pvlib-python
Examples:
>>> from math import radians
>>> from pvgisprototype.api.datetime.datetimeindex import generate_datetime_series
>>> timestamps = generate_datetime_series(start_time='2010-01-27', end_time='2010-01-28')
>>> from zoneinfo import ZoneInfo
>>> from pvgisprototype.api.position.azimuth import calculate_solar_azimuth_series_noaa
>>> solar_azimuth_series = calculate_solar_azimuth_series_noaa(
... longitude=radians(8.628),
... latitude=radians(45.812),
... timestamps=timestamps,
... timezone=ZoneInfo("UTC"),
... adjust_for_atmospheric_refraction=True
... )
>>> print(solar_azimuth_series)
solar_timing_algorithm='NOAA' solar_positioning_algorithm='NOAA' min_degrees=0 min_radians=0 data_source=None equation=None algorithm=None unit='radians' value=array([0.20324755, 0.68162394, 1.0347457 , 1.2956891 , 1.5057368 ,
1.6915469 , 1.8700204 , 2.0612502 , 2.250216 , 2.466558 ,
2.7086406 , 2.9751859 , 3.2346225 , 3.50884 , 3.757659 ,
3.9807858 , 4.1786404 , 4.3685203 , 4.547683 , 4.73073 ,
4.933111 , 5.178298 , 5.503539 , 5.949113 , 0.19983101],
dtype=float32) symbol='' description='Solar azimuth angle data for a location and period in time' label=None title='Solar Azimuth' supertitle='Solar Irradiance' shortname='Azimuth' name='Solar Azimuth' max_radians=6.283185307179586 max_degrees=360 definition='Solar azimuth angle' origin='North'
>>> print(solar_azimuth_series.degrees)
[ 11.6452265 39.054173 59.286556 74.23751 86.27236 96.918495
107.14427 118.10093 128.92787 141.32335 155.19366 170.46559
185.3302 201.04172 215.29799 228.08221 239.41844 250.29776
260.56302 271.05084 282.64642 296.6946 315.32956 340.85904
11.449472 ]
Source code in pvgisprototype/algorithms/noaa/solar_azimuth.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSolarAzimuthTimeSeriesNOAAInput)
def calculate_solar_azimuth_series_noaa(
longitude: Longitude, # radians
latitude: Latitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo | None,
adjust_for_atmospheric_refraction: bool = True,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output:bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAzimuth:
"""Calculate the solar azimuth angle (θ) for a time series at a specific
geographic latitude and longitude.
Calculate the solar azimuth angle (θ) for a time series at a specific
geographic latitude and longitude, by default correcting the solar zenith
angle for atmospheric refraction, as described in the following steps:
1. Calculate the solar declination, solar hour angle, and solar zenith
using NOAA's General Solar Position Calculations.
2. Calculate the solar azimuth angle using the equation reported in
Wikipedia's article "Solar azimuth angle" adjusting for the time of
day -- see also Notes :
- Morning (solar hour angle < 0): azimuth derived directly from the
arccosine function.
- Afternoon (solar hour angle > 0): azimuth adjusted to range in
[180°, 360°] by subtracting it from 360°.
Parameters
----------
longitude : float
Longitude of the location in radians.
latitude : float
Latitude of the location in radians.
timestamps : DatetimeIndex
Times for which the solar azimuth will be calculated.
timezone : ZoneInfo
Timezone of the location.
adjust_for_atmospheric_refraction : bool, optional
Whether to correct the solar zenith angle for atmospheric refraction.
dtype : str, optional
Data type for the calculations.
array_backend : str, optional
Backend array library to use.
verbose : int, optional
Verbosity level of the function.
log : int, optional
Log level for the function.
Returns
-------
SolarAzimuth
A custom data class that hold a NumPy NDArray of calculated solar
azimuth angles in radians, a method to convert the angles to degrees
and other metadata.
Notes
-----
From .. Excel sheet :
=IF(AC2>0,
MOD(
DEGREES(ACOS(((SIN(RADIANS($B$3))*COS(RADIANS(AD2)))-SIN(RADIANS(T2)))/(COS(RADIANS($B$3))*SIN(RADIANS(AD2)))))+180,
360),
MOD(
540-DEGREES(ACOS(((SIN(RADIANS($B$3))*COS(RADIANS(AD2)))-SIN(RADIANS(T2)))/(COS(RADIANS($B$3))*SIN(RADIANS(AD2))))),
360)
)
numerator = (sin(latitude.radians) * cos(solar_zenith.radians)) - sin(solar_declination.radians)
denominator = (cos(latitude.radians) * sin(solar_zenith.radians))
cosine_solar_azimuth = numerator / denominator
if solar_hour_angle > 0:
(pi + arccos(cosine_solar_azimuth)) % 2*pi
else:
(3*pi - arccos(cosine_solar_azimuth)) % 2*pi
Two important notes on the calculation of the solar azimuth angle :
- The equation implemented here follows upon the relevant Wikipedia article
for the "Solar azimuth angle" [1]_.
- The angle derived fom the arccosine function requires an adjustment to
correctly represent both morning and afternoon solar azimuth angles.
1. The equation given in NOAA's General Solar Position Calculations [0]_ is
sin(latitude) * cos(solar_zenith) - sin(solar_declination)
cos(180 - θ) = - ----------------------------------------------------------
cos(latitude) * sin(solar_zenith)
or after converting cos(180 - θ) to - cos(θ) :
sin(latitude) * cos(solar_zenith) - sin(solar_declination)
- cos(θ) = - ------------------------------------------------------------
cos(latitude) * sin(solar_zenith)
or :
sin(latitude) * cos(solar_zenith) - sin(solar_declination)
cos(θ) = ----------------------------------------------------------
cos(latitude) * sin(solar_zenith)
and finally :
sin(latitude) * cos(solar_zenith) - sin(solar_declination)
θ = arccos( -------------------------------------------------------------- )
cos(latitude) * sin(solar_zenith)
where θ is the wanted solar azimuth angle.
However, comparing this equation with the _almost identical_ equation
reported in Wikipedia's relevant article and subsection titled
"Conventional Trigonometric Formulas" [1]_, there seems to be a
difference of a minus sign :
sin(declination) - cos(zenith) * sin(latitude)
φs = arccos( ---------------------------------------------- )
sin(zenith) * cos(latitude)
where φs is the wanted solar azimuth angle.
A cross-comparison of the solar azimuth angle derived by the equation
reported in Wikipedia's article [1]_ and the libraries pvlib [2]_,
Skyfield [3]_ and suncalc [4]_, show a high agreement
across all of them.
2. Adjusting the solar azimuth based on the time of day
Given that arccosine ranges in [0, π] or else in [0°, 180°], the raw
calculated solar azimuth angle will likewise range in [0, π]. This
necessitates an adjustment based on the time of day.
- Morning (solar hour angle < 0): the azimuth angle is correctly
derived directly from the arccosine function representing angles from
the North clockwise to the South [0°, 180°].
- Afternoon (solar hour angle > 0): the azimuth angle needs to be
adjusted in order to correctly represent angles going further from
the South to the West [180°, 360°]. This is achieved by subtracting
the azimuth from 360°.
References
----------
.. [0] https://gml.noaa.gov/grad/solcalc/solareqns.PDF
.. [1] https://en.wikipedia.org/wiki/Solar_azimuth_angle#Conventional_Trigonometric_Formulas
.. [2] https://github.com/pvlib/pvlib-python
.. [3] https://github.com/skyfielders/python-skyfield/
.. [4] https://github.com/kylebarron/suncalc-py
Examples
--------
>>> from math import radians
>>> from pvgisprototype.api.datetime.datetimeindex import generate_datetime_series
>>> timestamps = generate_datetime_series(start_time='2010-01-27', end_time='2010-01-28')
>>> from zoneinfo import ZoneInfo
>>> from pvgisprototype.api.position.azimuth import calculate_solar_azimuth_series_noaa
>>> solar_azimuth_series = calculate_solar_azimuth_series_noaa(
... longitude=radians(8.628),
... latitude=radians(45.812),
... timestamps=timestamps,
... timezone=ZoneInfo("UTC"),
... adjust_for_atmospheric_refraction=True
... )
>>> print(solar_azimuth_series)
solar_timing_algorithm='NOAA' solar_positioning_algorithm='NOAA' min_degrees=0 min_radians=0 data_source=None equation=None algorithm=None unit='radians' value=array([0.20324755, 0.68162394, 1.0347457 , 1.2956891 , 1.5057368 ,
1.6915469 , 1.8700204 , 2.0612502 , 2.250216 , 2.466558 ,
2.7086406 , 2.9751859 , 3.2346225 , 3.50884 , 3.757659 ,
3.9807858 , 4.1786404 , 4.3685203 , 4.547683 , 4.73073 ,
4.933111 , 5.178298 , 5.503539 , 5.949113 , 0.19983101],
dtype=float32) symbol='\U000f19a5' description='Solar azimuth angle data for a location and period in time' label=None title='Solar Azimuth' supertitle='Solar Irradiance' shortname='Azimuth' name='Solar Azimuth' max_radians=6.283185307179586 max_degrees=360 definition='Solar azimuth angle' origin='North'
>>> print(solar_azimuth_series.degrees)
[ 11.6452265 39.054173 59.286556 74.23751 86.27236 96.918495
107.14427 118.10093 128.92787 141.32335 155.19366 170.46559
185.3302 201.04172 215.29799 228.08221 239.41844 250.29776
260.56302 271.05084 282.64642 296.6946 315.32956 340.85904
11.449472 ]
"""
solar_declination_series = calculate_solar_declination_series_noaa(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
solar_hour_angle_series = calculate_solar_hour_angle_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
solar_zenith_series = calculate_solar_zenith_series_noaa(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
timezone=timezone,
adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
numerator_series = sin(latitude.radians) * np.cos(
solar_zenith_series.radians
) - np.sin(solar_declination_series.radians)
denominator_series = cos(latitude.radians) * np.sin(solar_zenith_series.radians)
cosine_solar_azimuth_series = numerator_series / denominator_series
solar_azimuth_series = np.arccos(np.clip(cosine_solar_azimuth_series, -1, 1))
solar_azimuth_series = np.where(
solar_hour_angle_series.radians > 0, # afternoon hours !
np.mod((pi + solar_azimuth_series), 2 * pi),
np.mod(3 * pi - solar_azimuth_series, 2 * pi),
)
if validate_output:
if (
(solar_azimuth_series < SolarAzimuth().min_radians)
| (solar_azimuth_series > SolarAzimuth().max_radians)
).any():
out_of_range_values = solar_azimuth_series[
(solar_azimuth_series < SolarAzimuth().min_radians)
| (solar_azimuth_series > SolarAzimuth().max_radians)
]
# raise ValueError(# ?
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarAzimuth().min_radians}, {SolarAzimuth().max_radians}] radians"
f" in [code]solar_azimuth_series[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_azimuth_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAzimuth(
value=solar_azimuth_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.noaa,
solar_timing_algorithm=SolarTimeModel.noaa,
origin="North",
)
solar_declination ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_declination_series_noaa | Calculate the solar declination for a time series. |
calculate_solar_declination_series_noaa ¶
calculate_solar_declination_series_noaa(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarDeclination
Calculate the solar declination for a time series.
Notes
From NOAA's .. Excel sheet:
sine_solar_declination = ASIN( SIN(RADIANS(R2))*SIN(RADIANS(P2)) )
where:
P2 = M2-0.00569-0.00478*SIN(RADIANS(125.04-1934.136*G2))
where:
M2 = Geom Mean Long Sun (deg) + Geom Mean Anom Sun (deg)
R2 = Q2 + 0.00256 * COS(RADIANS(125.04 - 1934.136*G2))
where :
Q2 = Mean Obliq Ecliptic (deg)
Source code in pvgisprototype/algorithms/noaa/solar_declination.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSolarDeclinationTimeSeriesNOAAInput)
def calculate_solar_declination_series_noaa(
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output:bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarDeclination:
"""Calculate the solar declination for a time series.
Notes
-----
From NOAA's .. Excel sheet:
sine_solar_declination
= ASIN(
SIN(RADIANS(R2))*SIN(RADIANS(P2))
)
where:
P2 = M2-0.00569-0.00478*SIN(RADIANS(125.04-1934.136*G2))
where:
M2 = Geom Mean Long Sun (deg) + Geom Mean Anom Sun (deg)
R2 = Q2 + 0.00256 * COS(RADIANS(125.04 - 1934.136*G2))
where :
Q2 = Mean Obliq Ecliptic (deg)
"""
fractional_year_series = calculate_fractional_year_series_noaa(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
solar_declination_series = (
0.006918
- 0.399912 * np.cos(fractional_year_series.radians)
+ 0.070257 * np.sin(fractional_year_series.radians)
- 0.006758 * np.cos(2 * fractional_year_series.radians)
+ 0.000907 * np.sin(2 * fractional_year_series.radians)
- 0.002697 * np.cos(3 * fractional_year_series.radians)
+ 0.00148 * np.sin(3 * fractional_year_series.radians)
)
out_of_range = None
out_of_range_index = None
if validate_output:
out_of_range, out_of_range_index = identify_values_out_of_range_x(
series=solar_declination_series,
shape=timestamps.shape,
data_model=SolarDeclination(),
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_declination_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarDeclination(
value=solar_declination_series,
unit=RADIANS,
out_of_range=out_of_range if out_of_range is not None else None,
out_of_range_index=out_of_range_index if out_of_range is not None else None,
solar_positioning_algorithm=fractional_year_series.solar_positioning_algorithm,
)
solar_hour_angle ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_hour_angle_series_noaa | Calculate the solar hour angle for a time series. |
calculate_solar_hour_angle_series_noaa ¶
calculate_solar_hour_angle_series_noaa(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarHourAngle
Calculate the solar hour angle for a time series.
The solar hour angle calculation converts the local solar time (LST) into the number of degrees which the sun moves across the sky. In other words, it reflects the Earth's rotation and indicates the time of the day relative to the position of the Sun. It bases on the longitude and timestamp and by definition, the solar hour angle is :
- 0° at solar noon
- negative in the morning
- positive in the afternoon.
Since the Earth rotates 15° per hour (or pi / 12 in radians), each hour away from solar noon corresponds to an angular motion of the sun in the sky of 15°. Practically, the calculation converts a timestamp into a solar time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Returns | | required |
Notes
In the "original" equation, the solar hour angle is measured in degrees.
`hour_angle = true_solar_time / 4 - 180`
which is the same as
`hour_angle = true_solar_time * 0.25 - 180`
In the present implementation, we calculate the solar hour angle directly in radians. A full circle corresponds to 360 degrees or 2π radians. With 1440 minutes in a day, the angular change per minute is calculated as 2π radians divided by 1440 minutes. This results in approximately 0.004363323129985824 radians per minute.
To find the solar hour angle, we first calculate the time difference from solar noon (by subtracting the true solar time in minutes from 720). We then multiply this difference by the angular change per minute (0.004363323129985824) to convert the time difference into radians. This approach (accurately?) represents the solar hour angle as an angular measurement in radians, reflecting the Earth's rotation and the position of the sun in the sky relative to a given location on Earth.
In NREL's SPA ... , equation 32:
Η = ν + σ − α
Where :
- σ the observer geographical longitude, positive or negative
for east or west of Greenwich, respectively.
Limit Η to the range from 0 to 360 degrees using step 3.2.6 and note that it
is measured westward from south in this algorithm.
Step 3.2.6 :
Limit L to the range from 0 to 360 degrees. That can be
accomplished by dividing L by 360 and recording the decimal
fraction of the division as F. If L is positive, then the limited L
= 360 * F. If L is negative, then the limited L = 360 - 360 * F.
Source code in pvgisprototype/algorithms/noaa/solar_hour_angle.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSolarHourAngleTimeSeriesNOAAInput)
def calculate_solar_hour_angle_series_noaa(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output:bool = VALIDATE_OUTPUT_DEFAULT
) -> SolarHourAngle:
"""Calculate the solar hour angle for a time series.
The solar hour angle calculation converts the local solar time (LST) into
the number of degrees which the sun moves across the sky. In other words,
it reflects the Earth's rotation and indicates the time of the day relative
to the position of the Sun. It bases on the longitude and timestamp and by
definition, the solar hour angle is :
- 0° at solar noon
- negative in the morning
- positive in the afternoon.
Since the Earth rotates 15° per hour (or pi / 12 in radians), each hour
away from solar noon corresponds to an angular motion of the sun in the sky
of 15°. Practically, the calculation converts a timestamp into a solar
time.
Parameters
----------
Returns
-------
Notes
-----
In the "original" equation, the solar hour angle is measured in degrees.
`hour_angle = true_solar_time / 4 - 180`
which is the same as
`hour_angle = true_solar_time * 0.25 - 180`
In the present implementation, we calculate the solar hour angle directly
in radians. A full circle corresponds to 360 degrees or 2π radians. With
1440 minutes in a day, the angular change per minute is calculated as 2π
radians divided by 1440 minutes. This results in approximately
0.004363323129985824 radians per minute.
To find the solar hour angle, we first calculate the time difference from
solar noon (by subtracting the true solar time in minutes from 720). We
then multiply this difference by the angular change per minute
(0.004363323129985824) to convert the time difference into radians. This
approach (accurately?) represents the solar hour angle as an angular
measurement in radians, reflecting the Earth's rotation and the position of
the sun in the sky relative to a given location on Earth.
In NREL's SPA ... , equation 32:
Η = ν + σ − α
Where :
- σ the observer geographical longitude, positive or negative
for east or west of Greenwich, respectively.
Limit Η to the range from 0 to 360 degrees using step 3.2.6 and note that it
is measured westward from south in this algorithm.
Step 3.2.6 :
Limit L to the range from 0 to 360 degrees. That can be
accomplished by dividing L by 360 and recording the decimal
fraction of the division as F. If L is positive, then the limited L
= 360 * F. If L is negative, then the limited L = 360 - 360 * F.
"""
true_solar_time_series = calculate_true_solar_time_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
solar_hour_angle_series = (true_solar_time_series.minutes - 720.0) * (np.pi / 720.0)
# solar_hour_angle_series = np.where(
# # true_solar_time_series.minutes < 0,
# solar_hour_angle_series < 0,
# solar_hour_angle_series + pi,
# solar_hour_angle_series - pi,
# )
if validate_output:
if not np.all(
(SolarHourAngle().min_radians <= solar_hour_angle_series)
& (solar_hour_angle_series <= SolarHourAngle().max_radians)
):
out_of_range_values = solar_hour_angle_series[
~(
(-SolarHourAngle().min_radians <= solar_hour_angle_series)
& (solar_hour_angle_series <= SolarHourAngle().max_radians)
)
]
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarHourAngle().min_degrees}, {SolarHourAngle().max_degrees}] degrees"
f" in [code]solar_hour_angle_series[/code] : {np.degrees(out_of_range_values)}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_hour_angle_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarHourAngle(
value=solar_hour_angle_series,
unit=RADIANS,
solar_positioning_algorithm=SolarPositionModel.noaa,
solar_timing_algorithm=true_solar_time_series.timing_algorithm,
)
solar_time ¶
The true solar time based on NOAA's General Solar Position Calculations.
Functions:
| Name | Description |
|---|---|
calculate_true_solar_time_series_noaa | Calculate the true solar time at a specific geographic locations for a |
calculate_true_solar_time_series_noaa ¶
calculate_true_solar_time_series_noaa(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> TrueSolarTime
Calculate the true solar time at a specific geographic locations for a time series.
Calculate the true solar time at a specific geographic locations for a series of timestamps based on NOAA's General Solar Position Calculations [0]_.
The true solar time is the sum of the mean solar time and the equation of time. CONFIRM!?
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamps | DatetimeIndex | The timestamp to calculate offset for | required |
timezone | ZoneInfo | The timezone for calculation | required |
dtype | str | The data type for the calculations (the default is 'float32'). | DATA_TYPE_DEFAULT |
array_backend | str | The backend used for calculations (the default is 'NUMPY'). | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level | VERBOSE_LEVEL_DEFAULT |
log | int | Log level | LOG_LEVEL_DEFAULT |
Returns:
| Type | Description |
|---|---|
TrueSolarTime | |
Notes
The true solar time is the sum of the time offset and the true solar time in minutes.
time_offset = eqtime + 4*longitude – 60*timezone
See also
pysolar
In pysolar the true solar time is calculated as follows :
```
(math.tm_hour(when) * 60 + math.tm_min(when) + 4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
in which equation the last part is the time offset
```
4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
Additional notes:
From NOAA's General Solar Position Calculations
"Next, the true solar time is calculated in the following two
equations. First the time offset is found, in minutes, and then the
true solar time, in minutes."
time_offset = eqtime + 4*longitude – 60*timezone
where :
- eqtime is in minutes,
- longitude is in degrees (positive to the east of the Prime
Meridian),
- timezone is in hours from UTC (U.S. Mountain Standard Time = –7
hours).
tst = hr*60 + mn + sc/60 + time_offset
where :
- hr is the hour (0 - 23),
- mn is the minute (0 - 59),
- sc is the second (0 - 59).
References
Source code in pvgisprototype/algorithms/noaa/solar_time.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateTrueSolarTimeTimeSeriesNOAAInput)
def calculate_true_solar_time_series_noaa(
longitude: Longitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> TrueSolarTime:
"""Calculate the true solar time at a specific geographic locations for a
time series.
Calculate the true solar time at a specific geographic locations for a
series of timestamps based on NOAA's General Solar Position Calculations
[0]_.
The true solar time is the sum of the mean solar time and the equation of
time. CONFIRM!?
Parameters
----------
timestamps: DatetimeIndex, optional
The timestamp to calculate offset for
timezone: str, optional
The timezone for calculation
dtype : str, optional
The data type for the calculations (the default is 'float32').
array_backend : str, optional
The backend used for calculations (the default is 'NUMPY').
verbose: int
Verbosity level
log: int
Log level
Returns
-------
TrueSolarTime
Notes
-----
The true solar time is the sum of the time offset and the true solar time
in minutes.
time_offset = eqtime + 4*longitude – 60*timezone
See also
--------
pysolar
In pysolar the true solar time is calculated as follows :
```
(math.tm_hour(when) * 60 + math.tm_min(when) + 4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
in which equation the last part is the time offset
```
4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
Additional notes:
From NOAA's General Solar Position Calculations
"Next, the true solar time is calculated in the following two
equations. First the time offset is found, in minutes, and then the
true solar time, in minutes."
time_offset = eqtime + 4*longitude – 60*timezone
where :
- eqtime is in minutes,
- longitude is in degrees (positive to the east of the Prime
Meridian),
- timezone is in hours from UTC (U.S. Mountain Standard Time = –7
hours).
tst = hr*60 + mn + sc/60 + time_offset
where :
- hr is the hour (0 - 23),
- mn is the minute (0 - 59),
- sc is the second (0 - 59).
References
----------
.. [0] https://gml.noaa.gov/grad/solcalc/solareqns.PDF
"""
time_offset_series = calculate_time_offset_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
validate_output=validate_output,
)
true_solar_time_series = (
timestamps - timestamps.normalize()
).total_seconds() + time_offset_series.value * 60
# array_parameters = {
# "shape": true_solar_time_series.shape,
# "dtype": dtype,
# "init_method": "zeros",
# "backend": array_backend,
# }
# true_solar_time_series_in_minutes = create_array(**array_parameters)
true_solar_time_series_in_minutes = mod(
(true_solar_time_series).astype(dtype) / 60, 1440
)
if validate_output:
if not (
(TrueSolarTime().min_minutes <= true_solar_time_series_in_minutes)
& (true_solar_time_series_in_minutes <= TrueSolarTime().max_minutes)
).all():
out_of_range_values = true_solar_time_series_in_minutes[
~(
(TrueSolarTime().min_minutes <= true_solar_time_series_in_minutes)
& (true_solar_time_series_in_minutes <= TrueSolarTime().max_minutes)
)
]
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{TrueSolarTime().min_minutes}, {TrueSolarTime().max_minutes}] minutes"
f" in [code]true_solar_time_series_in_minutes[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=true_solar_time_series_in_minutes,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return TrueSolarTime(
value=array(true_solar_time_series_in_minutes, dtype=dtype),
unit=MINUTES,
timing_algorithm=SolarTimeModel.noaa,
)
solar_zenith ¶
Functions:
| Name | Description |
|---|---|
adjust_solar_zenith_for_atmospheric_refraction_time_series | Adjust solar zenith for atmospheric refraction for a time series of solar zenith angles |
atmospheric_refraction_adjustment | Vectorized calculation of atmospheric refraction adjustment for different solar altitudes. |
calculate_solar_zenith_series_noaa | Calculate the solar zenith angle for a location over a time series |
adjust_solar_zenith_for_atmospheric_refraction_time_series ¶
adjust_solar_zenith_for_atmospheric_refraction_time_series(
solar_zenith_series: SolarZenith,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarZenith
Adjust solar zenith for atmospheric refraction for a time series of solar zenith angles
Source code in pvgisprototype/algorithms/noaa/solar_zenith.py
@validate_with_pydantic(AdjustSolarZenithForAtmosphericRefractionTimeSeriesNOAAInput)
def adjust_solar_zenith_for_atmospheric_refraction_time_series(
solar_zenith_series: SolarZenith,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
validate_output:bool = VALIDATE_OUTPUT_DEFAULT
) -> SolarZenith:
"""Adjust solar zenith for atmospheric refraction for a time series of solar zenith angles"""
# Mask
solar_altitude_series_array = (
np.radians(90, dtype=dtype) - solar_zenith_series.radians
) # in radians
# Adjust using vectorized operations
adjustment = atmospheric_refraction_adjustment(solar_altitude_series_array, dtype=dtype)
adjusted_solar_zenith_series_array = solar_zenith_series.radians - adjustment
# Validate
'''
if not np.all(np.isfinite(adjusted_solar_zenith_series_array)) or not np.all(
(SolarZenith().min_radians <= adjusted_solar_zenith_series_array)
& (adjusted_solar_zenith_series_array <= SolarZenith().max_radians)
):
raise ValueError(
f"The `adjusted_solar_zenith` should be a finite number ranging in [{SolarZenith().min_radians}, {SolarZenith().max_radians}] radians"
)
'''
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
return SolarZenith(
value=adjusted_solar_zenith_series_array,
unit=RADIANS,
solar_positioning_algorithm=solar_zenith_series.solar_positioning_algorithm,
solar_timing_algorithm=solar_zenith_series.solar_timing_algorithm,
adjusted_for_atmospheric_refraction=True, # This is what this function does !
)
atmospheric_refraction_adjustment ¶
Vectorized calculation of atmospheric refraction adjustment for different solar altitudes. Applies different formulas based on the solar altitude angle.
Source code in pvgisprototype/algorithms/noaa/solar_zenith.py
def atmospheric_refraction_adjustment(
solar_altitude: np.ndarray, # radians
dtype: str = "float64"
) -> np.ndarray:
"""
Vectorized calculation of atmospheric refraction adjustment for different solar altitudes.
Applies different formulas based on the solar altitude angle.
"""
tangent_solar_altitude = np.tan(solar_altitude)
# Conditions
mask_high = solar_altitude > np.radians(5, dtype=dtype)
mask_near = (solar_altitude > np.radians(-0.575, dtype=dtype)) & ~mask_high
mask_below = solar_altitude <= np.radians(-0.575, dtype=dtype)
# High solar altitude adjustment
adjustment_high = (
58.1 / tangent_solar_altitude
- 0.07 / (tangent_solar_altitude**3)
+ 0.000086 / (tangent_solar_altitude**5)
) / 3600 # 1 degree / 3600 seconds
# Near horizon adjustment
solar_altitude_deg = np.degrees(solar_altitude)
adjustment_near = (
1735
+ solar_altitude_deg
* (
-518.2
+ solar_altitude_deg
* (103.4 + solar_altitude_deg * (-12.79 + solar_altitude_deg * 0.711))
)
) / 3600 # 1 degree / 3600 seconds
# Below horizon adjustment
adjustment_below = (-20.774 / tangent_solar_altitude) / 3600 # 1 degree / 3600 seconds
# Initialize adjustment array
adjustment = np.zeros_like(solar_altitude, dtype=dtype)
# Apply adjustments based on conditions
adjustment[mask_high] = np.radians(adjustment_high[mask_high])
adjustment[mask_near] = np.radians(adjustment_near[mask_near])
adjustment[mask_below] = np.radians(adjustment_below[mask_below])
return adjustment
calculate_solar_zenith_series_noaa ¶
calculate_solar_zenith_series_noaa(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo,
adjust_for_atmospheric_refraction: bool = False,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarZenith
Calculate the solar zenith angle for a location over a time series
Source code in pvgisprototype/algorithms/noaa/solar_zenith.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateSolarZenithTimeSeriesNOAAInput)
def calculate_solar_zenith_series_noaa(
longitude: Longitude,
latitude: Latitude, # radians
timestamps: DatetimeIndex,
timezone: ZoneInfo,
# solar_hour_angle_series: SolarHourAngle,
adjust_for_atmospheric_refraction: bool = False,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = 0,
log: int = 0,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarZenith:
"""Calculate the solar zenith angle for a location over a time series"""
solar_declination_series = calculate_solar_declination_series_noaa(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
validate_output=validate_output,
)
solar_hour_angle_series = calculate_solar_hour_angle_series_noaa(
longitude=longitude,
timestamps=timestamps,
timezone=timezone,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
validate_output=validate_output,
)
cosine_solar_zenith = sin(latitude.radians) * np.sin(
solar_declination_series.radians
) + cos(latitude.radians) * np.cos(solar_declination_series.radians) * np.cos(
solar_hour_angle_series.radians
)
solar_zenith_series = SolarZenith(
value=np.arccos(cosine_solar_zenith), # Important !
unit=RADIANS,
solar_positioning_algorithm=solar_declination_series.solar_positioning_algorithm,
solar_timing_algorithm=solar_hour_angle_series.solar_timing_algorithm,
)
if adjust_for_atmospheric_refraction:
solar_zenith_series = (
adjust_solar_zenith_for_atmospheric_refraction_time_series(
solar_zenith_series,
)
)
if validate_output:
if not np.all(np.isfinite(solar_zenith_series.radians)) or not np.all(
(SolarZenith().min_radians <= solar_zenith_series.radians)
& (solar_zenith_series.radians <= SolarZenith().max_radians)
):
raise ValueError(
f"Solar zenith values should be finite numbers and range in [{SolarZenith().min_radians}, {SolarZenith().max_radians}] radians"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_zenith_series.value,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return solar_zenith_series
time_offset ¶
The time offset based on NOAA's General Solar Position Calculations.
Functions:
| Name | Description |
|---|---|
calculate_time_offset_series_noaa | Calculate the variation of the local solar time within a |
calculate_time_offset_series_noaa ¶
calculate_time_offset_series_noaa(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo = ZONEINFO_UTC,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> TimeOffset
Calculate the variation of the local solar time within a given timezone for a time series.
Calculate the variation of the local solar time, also referred to as time offset, within a timezone for NOAA's General Solar Position Calculations [0]_.
The time offset (in minutes) incorporates the Equation of Time and accounts for the variation of the Local Solar Time (LST) within a given timezone due to the longitude variations within the time zone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
longitude | Longitude | The longitude for calculation in radians (note: differs from the original equation which expects degrees). | required |
timestamps | DatetimeIndex | The timestamp to calculate the offset for | required |
timezone | ZoneInfo | The timezone | ZONEINFO_UTC |
dtype | str | The data type for the calculations (the default is 'float32'). | DATA_TYPE_DEFAULT |
array_backend | str | The backend used for calculations (the default is 'NUMPY'). | ARRAY_BACKEND_DEFAULT |
verbose | int | Verbosity level | VERBOSE_LEVEL_DEFAULT |
log | int | Log level | LOG_LEVEL_DEFAULT |
Returns:
| Name | Type | Description |
|---|---|---|
TimeOffset | TimeOffset | The time offset for a single or a series of timestamps. |
Notes
The equation given in NOAA's General Solar Position Calculations [0]_ is
time_offset = eqtime + 4*longitude – 60*timezone
where (variable name and units):
- time_offset, minutes
- longitude, degrees
- timezone, hours
- eqtime, minutes
See also
pysolar
In pysolar the true solar time is calculated as follows :
```
(math.tm_hour(when) * 60 + math.tm_min(when) + 4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
in which equation the time offset is the last part.
```
4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
Another reference
TC = 4 * (Longitude - LSTM) + EoT
where:
- TC : Time Correction Factor, minutes
- Longitude : Geographical Longitude, degrees
- LSTM : Local Standard Time Meridian, degrees * hours
- EoT : Equation of Time
where:
- `LSTM = 15 degrees * ΔTUTC`
where:
- ΔTUTC = LT - UTC, hours
where:
- ΔTUTC : difference of LT from UTC in hours
- LT : Local Time
- UTC : Universal Coordinated Time
- The factor 4 (minutes) comes from the fact that the Earth
rotates 1° every 4 minutes.
Examples:
Mount Olympus is UTC + 2, hence LSTM = 15 * 2 = 30 deg. East
The time offset in minutes ranges in
[ -720 (Longitude) - 720 (Up to -12 TimeZones) - 20 (Equation of Time) = -1460,
+720 (Longitude) + 840 (Up to +14 TimeZones) + 20 (Equation of Time) = 1580 ]
The valid ranges of the components that contribute to the time offset are:
-
Geographical longitude ranges from west to east in [-180, 180] degrees. A day is approximately 1440 minutes, hence converting the degrees to minutes, the longitude ranges in [-720, 720] minutes.
-
The timezone offset from the Coordinated Universal Time (UTC), considering time zones that are offset by unusual amounts of time from UTC, ranges from west of UTC to east of UTC in [-12, 14] hours or [-720, 840] minutes.
-
The Equation of Time accounts for the variations in the Earth's orbital speed and axial tilt. It varies throughout the year, but is typically within the range of about -20 minutes to +20 minutes.
References
Source code in pvgisprototype/algorithms/noaa/time_offset.py
@log_function_call
@custom_cached
@validate_with_pydantic(CalculateTimeOffsetTimeSeriesNOAAInput)
def calculate_time_offset_series_noaa(
longitude: Longitude,
timestamps: DatetimeIndex,
timezone: ZoneInfo = ZONEINFO_UTC,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> TimeOffset:
"""Calculate the variation of the local solar time within a
given timezone for a time series.
Calculate the variation of the local solar time, also referred to as _time
offset_, within a timezone for NOAA's General Solar Position Calculations
[0]_.
The time offset (in minutes) incorporates the Equation of Time and
accounts for the variation of the Local Solar Time (LST) within a given
timezone due to the longitude variations within the time zone.
Parameters
----------
longitude: float
The longitude for calculation in radians (note: differs from the original
equation which expects degrees).
timestamps: DatetimeIndex
The timestamp to calculate the offset for
timezone: ZoneInfo, optional
The timezone
dtype : str, optional
The data type for the calculations (the default is 'float32').
array_backend : str, optional
The backend used for calculations (the default is 'NUMPY').
verbose: int, optional
Verbosity level
log: int, optional
Log level
Returns
-------
TimeOffset:
The time offset for a single or a series of timestamps.
Notes
-----
The equation given in NOAA's General Solar Position Calculations [0]_ is
time_offset = eqtime + 4*longitude – 60*timezone
where (variable name and units):
- time_offset, minutes
- longitude, degrees
- timezone, hours
- eqtime, minutes
See also
--------
pysolar
In pysolar the true solar time is calculated as follows :
```
(math.tm_hour(when) * 60 + math.tm_min(when) + 4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
in which equation the time offset is the last part.
```
4 * longitude_deg + equation_of_time(math.tm_yday(when)))
```
Another reference
TC = 4 * (Longitude - LSTM) + EoT
where:
- TC : Time Correction Factor, minutes
- Longitude : Geographical Longitude, degrees
- LSTM : Local Standard Time Meridian, degrees * hours
- EoT : Equation of Time
where:
- `LSTM = 15 degrees * ΔTUTC`
where:
- ΔTUTC = LT - UTC, hours
where:
- ΔTUTC : difference of LT from UTC in hours
- LT : Local Time
- UTC : Universal Coordinated Time
- The factor 4 (minutes) comes from the fact that the Earth
rotates 1° every 4 minutes.
Examples:
Mount Olympus is UTC + 2, hence LSTM = 15 * 2 = 30 deg. East
The time offset in minutes ranges in
[ -720 (Longitude) - 720 (Up to -12 TimeZones) - 20 (Equation of Time) = -1460,
+720 (Longitude) + 840 (Up to +14 TimeZones) + 20 (Equation of Time) = 1580 ]
The valid ranges of the components that contribute to the time offset are:
- Geographical longitude ranges from west to east in [-180, 180] degrees.
A day is approximately 1440 minutes, hence converting the degrees to
minutes, the longitude ranges in [-720, 720] minutes.
- The timezone offset from the Coordinated Universal Time (UTC),
considering time zones that are offset by unusual amounts of time from
UTC, ranges from west of UTC to east of UTC in [-12, 14] hours or [-720,
840] minutes.
- The Equation of Time accounts for the variations in the Earth's orbital
speed and axial tilt. It varies throughout the year, but is typically
within the range of about -20 minutes to +20 minutes.
References
----------
.. [0] https://gml.noaa.gov/grad/solcalc/solareqns.PDF
"""
local_standard_time_meridian_minutes_series = 0 # in UTC the offest is 0
if timezone and timezone != ZONEINFO_UTC:
# We need the .tz attribute to compare with the user-requested timezone !
if not timestamps.tz:
timestamps = timestamps.tz_localize(timezone)
# Explain why this is necessary !-------------- Further Optimisation ?
unique_timezone_offsets_in_minutes = {
stamp.tzinfo: stamp.tzinfo.utcoffset(stamp).total_seconds() / 60
for stamp in timestamps if stamp.tzinfo is not None
}
local_standard_time_meridian_minutes_series = numpy.array(
[
unique_timezone_offsets_in_minutes[stamp.tzinfo]
for stamp in timestamps
if stamp.tzinfo is not None
],
dtype=dtype,
)
# # ------------------------------------------- Further Optimisation ?
equation_of_time_series = calculate_equation_of_time_series_noaa(
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
validate_output=validate_output,
)
time_offset_series_in_minutes = (
longitude.as_minutes
- local_standard_time_meridian_minutes_series
+ equation_of_time_series.minutes
)
if validate_output:
if not numpy.all(
(TimeOffset().min_minutes <= time_offset_series_in_minutes)
& (time_offset_series_in_minutes <= TimeOffset().max_minutes)
):
index_of_out_of_range_values = np.where(
(time_offset_series_in_minutes < TimeOffset().min_minutes)
| (time_offset_series_in_minutes > TimeOffset().max_minutes)
)
out_of_range_values = time_offset_series_in_minutes[
index_of_out_of_range_values
]
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{TimeOffset().min_minutes}, {TimeOffset().max_minutes}] minutes"
f" in [code]time_offset_series_in_minutes[/code] : {out_of_range_values}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=time_offset_series_in_minutes,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return TimeOffset(
value=time_offset_series_in_minutes,
unit=MINUTES,
solar_positioning_algorithm="NOAA",
solar_timing_algorithm="NOAA",
)
pelland ¶
Modules:
| Name | Description |
|---|---|
spectral_effect | |
spectral_effect ¶
Functions:
| Name | Description |
|---|---|
calculate_spectral_factor_pelland | Calculate the spectral factor for each PV technology based on |
calculate_spectral_factor_pelland ¶
calculate_spectral_factor_pelland(
irradiance: DataArray,
responsivity: DataFrame,
reference_spectrum: DataFrame,
) -> DataFrame
Calculate the spectral factor for each PV technology based on Pelland 2022.
Notes
Some Python source code shared via personal communication.
Source code in pvgisprototype/algorithms/pelland/spectral_effect.py
@log_function_call
def calculate_spectral_factor_pelland(
irradiance: DataArray,
responsivity: DataFrame,
reference_spectrum: DataFrame,
) -> DataFrame:
"""Calculate the spectral factor for each PV technology based on
Pelland 2022.
Notes
-----
Some Python source code shared via personal communication.
"""
# Verify the reference spectrum is valid (no zero or near-zero values) ?
# if (reference_spectrum <= 0).any():
# raise ValueError(
# "The reference spectrum contains zero or negative values, which are invalid."
# )
# else:
# print(f'Reference spectrum input : {reference_spectrum}')
# Align wavelengths (columns) between dataframes
# if not isinstance(irradiance, DataFrame):
# irradiance = irradiance.to_dataframe()
# wavelengths = irradiance.columns.intersection(responsivity.index).intersection(
# reference_spectrum.columns
# )
import numpy
logger.debug(
f"Wavelengths in `irradiance` input : {irradiance.center_wavelength.values}",
)
logger.debug(
f"Wavelengths in `responsivity` input : {responsivity}",
)
logger.debug(
f"Wavelengths in `reference_spectrum` input : {reference_spectrum.columns}"
)
# Common wavelength range
common_wavelengths = numpy.intersect1d(
irradiance.center_wavelength.values,
numpy.intersect1d(responsivity.center_wavelength, reference_spectrum.columns),
)
logger.debug(
f"Intersection of wavelengths across input data : {common_wavelengths}"
)
# in Pelland : useful reference spectrum > average over the reference spectrum
responsivity_selected = responsivity.sel(center_wavelength=common_wavelengths)
logger.debug(f"Selected responsivity data : {responsivity_selected}")
reference_spectrum_selected = reference_spectrum.loc["global", common_wavelengths]
logger.debug(f"Selected reference spectrum data : {reference_spectrum_selected}")
# in Pelland 2022 : useful fraction of reference spectrum
reference_current_density = (
responsivity_selected * reference_spectrum_selected
).sum() / reference_spectrum_selected.sum()
logger.debug(
f"Reference Current Density : {reference_current_density}",
alt=f"[bold][yellow]Reference[/yellow] current density[/bold] : {reference_current_density}",
)
# useful irradiance (time-varying)
irradiance_selected = irradiance.sel(center_wavelength=common_wavelengths)
logger.debug(f"Selected irradiance data : {irradiance_selected}")
sum_of_responsivity_by_irradiance = (
responsivity_selected * irradiance_selected
).sum(dim="center_wavelength")
logger.debug(
f"Sum of responsivity * irradiance : {sum_of_responsivity_by_irradiance}"
)
sum_of_irradiance = (irradiance_selected).sum(dim="center_wavelength")
logger.debug(f"Sum of irradiance : {sum_of_irradiance}")
observed_current_density = (responsivity_selected * irradiance_selected).sum(
dim="center_wavelength"
) / irradiance_selected.sum(dim="center_wavelength")
logger.debug(f"Current Density of Observed Irradiance : {observed_current_density}")
spectral_factor = observed_current_density / reference_current_density
logger.debug(f"Spectral factor = {spectral_factor}")
components_container = {
"Metadata": lambda: {},
"Spectral factor": lambda: {
TITLE_KEY_NAME: SPECTRAL_FACTOR_NAME,
SPECTRAL_FACTOR_COLUMN_NAME: spectral_factor.to_numpy(),
}, # if verbose > 0 else {},
"Inputs": lambda: {
"Irradiance": irradiance,
"Responsivity": responsivity,
"Reference spectrum": reference_spectrum,
},
"Intermediate quantities": lambda: {
"Common spectral wavelengths": common_wavelengths,
"Selected spectral responsivity": responsivity_selected,
"Selected observed irradiance": irradiance_selected,
"Selected reference spectrum": reference_spectrum_selected,
},
"Sum of quantities": lambda: {
"Sum of Irradiance": sum_of_irradiance,
"Sum of responsivity by irradiance": sum_of_responsivity_by_irradiance,
"Sum of Reference spectrum": reference_spectrum.sum(),
},
# "Energy" : lambda: {
# 'Reference energy': total_reference_energy,
# 'Observed energy': total_observed_energy,
# },
"Current density": lambda: {
"Reference current": reference_current_density,
"Observed current": observed_current_density,
},
# if verbose > 1
# else {},
}
components = {}
for _, component in components_container.items():
components.update(component())
return SpectralFactorSeries(
value=spectral_factor.to_numpy(),
unit=UNITLESS,
spectral_factor_algorithm="Pelland 2022",
components=components,
)
pvlib ¶
Modules:
| Name | Description |
|---|---|
solar_altitude | |
solar_azimuth | |
solar_declination | |
solar_hour_angle | |
solar_incidence | |
solar_zenith | |
solar_altitude ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_altitude_series_pvlib | Calculate the solar altitude (θ) |
calculate_solar_altitude_series_pvlib ¶
calculate_solar_altitude_series_pvlib(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAltitude
Calculate the solar altitude (θ)
Source code in pvgisprototype/algorithms/pvlib/solar_altitude.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarAltitudePVLIBInputModel)
def calculate_solar_altitude_series_pvlib(
longitude: Longitude, # degrees
latitude: Latitude, # degrees
timestamps: DatetimeIndex,
# timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAltitude:
"""Calculate the solar altitude (θ)"""
solar_position = get_solarposition(
timestamps, latitude.degrees, longitude.degrees
)
solar_altitude_series = solar_position["apparent_elevation"].values
if not numpy.all(numpy.isfinite(solar_altitude_series)) or not numpy.all(
(SolarAltitude().min_degrees <= solar_altitude_series)
& (solar_altitude_series <= SolarAltitude().max_degrees)
):
raise ValueError(
f"Solar altitude values should be finite numbers and range in [{SolarAltitude().min_degrees}, {SolarAltitude().max_degrees}] degrees"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_altitude_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAltitude(
value=solar_altitude_series,
unit=DEGREES,
solar_positioning_algorithm="pvlib",
solar_timing_algorithm="pvlib",
)
solar_azimuth ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_azimuth_series_pvlib | Calculate the solar azimuth (θ) |
calculate_solar_azimuth_series_pvlib ¶
calculate_solar_azimuth_series_pvlib(
longitude: Longitude,
latitude: Latitude,
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAzimuth
Calculate the solar azimuth (θ)
Source code in pvgisprototype/algorithms/pvlib/solar_azimuth.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarAzimuthPVLIBInputModel)
def calculate_solar_azimuth_series_pvlib(
longitude: Longitude, # degrees
latitude: Latitude, # degrees
timestamps: DatetimeIndex,
# timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarAzimuth:
"""Calculate the solar azimuth (θ)"""
solar_position = pvlib.solarposition.get_solarposition(
timestamps, latitude.degrees, longitude.degrees
)
azimuth_origin = "North"
solar_azimuth_series = solar_position["azimuth"].values
if not numpy.all(numpy.isfinite(solar_azimuth_series)) or not numpy.all(
(SolarAzimuth().min_degrees <= solar_azimuth_series)
& (solar_azimuth_series <= SolarAzimuth().max_degrees)
):
raise ValueError(
f"Solar azimuth values should be finite numbers and range in [{SolarAzimuth().min_degrees}, {SolarAzimuth().max_degrees}] degrees"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_azimuth_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarAzimuth(
value=solar_azimuth_series,
unit=DEGREES,
solar_positioning_algorithm="pvlib",
solar_timing_algorithm="pvlib",
origin=azimuth_origin,
)
solar_declination ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_declination_series_pvlib | Calculate the solar declination in radians |
calculate_solar_declination_series_pvlib ¶
Calculate the solar declination in radians
Source code in pvgisprototype/algorithms/pvlib/solar_declination.py
@custom_cached
@validate_with_pydantic(CalculateSolarDeclinationSeriesPVLIBInput)
def calculate_solar_declination_series_pvlib(
timestamps: DatetimeIndex,
) -> SolarDeclination:
"""Calculate the solar declination in radians"""
days_of_year = timestamps.dayofyear
solar_declination_series = pvlib.solarposition.declination_spencer71(days_of_year)
# solar_declination = pvlib.solarposition.declination_spencer71(doy)
# if (
# not isfinite(solar_declination.degrees)
# or not solar_declination.min_degrees <= solar_declination.degrees <= solar_declination.max_degrees
# ):
# raise ValueError(
# f"The calculated solar declination angle {solar_declination.degrees} is out of the expected range\
# [{solar_declination.min_degrees}, {solar_declination.max_degrees}] degrees"
# )
return SolarDeclination(
value=solar_declination_series.values,
unit=RADIANS,
solar_positioning_algorithm="pvlib (Spencer, 1971)",
solar_timing_algorithm="pvlib",
)
solar_hour_angle ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_hour_angle_series_pvlib | Calculate the solar hour angle in radians. |
calculate_solar_hour_angle_series_pvlib ¶
calculate_solar_hour_angle_series_pvlib(
longitude: Longitude,
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarHourAngle
Calculate the solar hour angle in radians.
Source code in pvgisprototype/algorithms/pvlib/solar_hour_angle.py
@custom_cached
@validate_with_pydantic(SolarHourAngleSeriesPVLIBInput)
def calculate_solar_hour_angle_series_pvlib(
longitude: Longitude,
timestamps: DatetimeIndex,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarHourAngle:
"""Calculate the solar hour angle in radians."""
equation_of_time_series = pvlib.solarposition.equation_of_time_spencer71(
timestamps.dayofyear
)
solar_hour_angle_series = pvlib.solarposition.hour_angle(
timestamps, longitude.degrees, equation_of_time=equation_of_time_series
).to_numpy()
if not numpy.all(
(SolarHourAngle().min_degrees <= solar_hour_angle_series)
& (solar_hour_angle_series <= SolarHourAngle().max_degrees)
):
out_of_range_values = solar_hour_angle_series[
~(
(-SolarHourAngle().min_degrees <= solar_hour_angle_series)
& (solar_hour_angle_series <= SolarHourAngle().max_degrees)
)
]
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarHourAngle().min_degrees}, {SolarHourAngle().max_degrees}] degrees"
f" in [code]solar_hour_angle_series[/code] : {numpy.degrees(out_of_range_values)}"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_hour_angle_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarHourAngle(
value=solar_hour_angle_series,
unit=DEGREES,
solar_positioning_algorithm=SolarPositionModel.pvlib,
solar_timing_algorithm=SolarTimeModel.pvlib + " (Spencer 1971)",
)
solar_incidence ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_incidence_series_pvlib | Calculate the solar incidence (θ) |
calculate_solar_incidence_series_pvlib ¶
calculate_solar_incidence_series_pvlib(
longitude: Longitude,
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex = now_utc_datetimezone(),
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence
Calculate the solar incidence (θ)
Source code in pvgisprototype/algorithms/pvlib/solar_incidence.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarIncidencePVLIBInputModel)
def calculate_solar_incidence_series_pvlib(
longitude: Longitude,
latitude: Latitude,
surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
timestamps: DatetimeIndex = now_utc_datetimezone(),
# adjust_for_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence:
"""Calculate the solar incidence (θ)"""
solar_position = pvlib.solarposition.get_solarposition(
timestamps, latitude.degrees, longitude.degrees
)
solar_zenith_series = calculate_solar_zenith_series_pvlib(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
solar_azimuth_series = calculate_solar_azimuth_series_pvlib(
longitude=longitude,
latitude=latitude,
timestamps=timestamps,
dtype=dtype,
array_backend=array_backend,
verbose=verbose,
log=log,
)
solar_incidence_series_in_degrees = pvlib.irradiance.aoi(
surface_tilt=surface_tilt.degrees,
surface_azimuth=surface_orientation.degrees,
solar_zenith=solar_zenith_series.degrees,
solar_azimuth=solar_azimuth_series.degrees,
)
# solar_incidence_series = solar_position["apparent_elevation"].values
incidence_angle_definition = SolarIncidence().definition
incidence_angle_description = SolarIncidence().description
if complementary_incidence_angle:
logger.debug(
f":information: Converting solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}...",
alt=f":information: [bold][magenta]Converting[/magenta] solar incidence angle to {COMPLEMENTARY_INCIDENCE_ANGLE_DEFINITION}[/bold]...",
)
solar_incidence_series_in_degrees = 90 - solar_incidence_series_in_degrees
incidence_angle_definition = SolarIncidence().definition_complementary
incidence_angle_description = SolarIncidence().description_complementary
if zero_negative_solar_incidence_angle:
# set negative or below horizon angles ( == solar zenith > 90 ) to 0 !
solar_incidence_series_in_degrees[
(solar_incidence_series_in_degrees < 0) | (solar_zenith_series.degrees > 90)
] = NO_SOLAR_INCIDENCE
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_incidence_series_in_degrees,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
from pvgisprototype.constants import DEGREES
return SolarIncidence(
value=solar_incidence_series_in_degrees,
unit=DEGREES,
solar_positioning_algorithm=solar_zenith_series.solar_solar_positioning_algorithm,
solar_timing_algorithm=solar_zenith_series.solar_timing_algorithm,
incidence_algorithm=SolarIncidenceModel.pvlib,
definition=incidence_angle_definition,
description=incidence_angle_description,
azimuth_origin=solar_azimuth_series.origin,
)
solar_zenith ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_zenith_series_pvlib | Calculate the solar azimith (θ) in radians |
calculate_solar_zenith_series_pvlib ¶
calculate_solar_zenith_series_pvlib(
longitude: Longitude,
latitude: Latitude,
timestamps: datetime,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarZenith
Calculate the solar azimith (θ) in radians
Source code in pvgisprototype/algorithms/pvlib/solar_zenith.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarZenithPVLIBInputModel)
def calculate_solar_zenith_series_pvlib(
longitude: Longitude, # degrees
latitude: Latitude, # degrees
timestamps: datetime,
# timezone: ZoneInfo,
dtype: str = DATA_TYPE_DEFAULT,
array_backend: str = ARRAY_BACKEND_DEFAULT,
verbose: int = VERBOSE_LEVEL_DEFAULT,
log: int = LOG_LEVEL_DEFAULT,
) -> SolarZenith:
"""Calculate the solar azimith (θ) in radians"""
solar_position = pvlib.solarposition.get_solarposition(
timestamps, latitude.degrees, longitude.degrees
)
solar_zenith_series = solar_position["zenith"].values
if not numpy.all(numpy.isfinite(solar_zenith_series)) or not numpy.all(
(SolarZenith().min_degrees <= solar_zenith_series)
& (solar_zenith_series <= SolarZenith().max_degrees)
):
raise ValueError(
f"Solar zenith values should be finite numbers and range in [{SolarZenith().min_degrees}, {SolarZenith().max_degrees}] degrees"
)
if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
debug(locals())
log_data_fingerprint(
data=solar_zenith_series,
log_level=log,
hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
)
return SolarZenith(
value=solar_zenith_series,
unit=DEGREES,
solar_positioning_algorithm="pvlib",
solar_timing_algorithm="pvlib",
)
skyfield ¶
Modules:
| Name | Description |
|---|---|
solar_geometry | |
solar_time | |
sunrise_and_sunset | |
solar_geometry ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_altitude_azimuth_skyfield | Calculate sun position |
calculate_solar_hour_angle_declination_skyfield | Calculate the hour angle ω' |
calculate_solar_position_skyfield | Calculate sun position above the local horizon using Skyfield. |
calculate_solar_altitude_azimuth_skyfield ¶
calculate_solar_altitude_azimuth_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
) -> Tuple[SolarAltitude, SolarAzimuth]
Calculate sun position
Source code in pvgisprototype/algorithms/skyfield/solar_geometry.py
@validate_with_pydantic(CalculateSolarAltitudeAzimuthSkyfieldInputModel)
def calculate_solar_altitude_azimuth_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
) -> Tuple[SolarAltitude, SolarAzimuth]:
"""Calculate sun position"""
solar_position = calculate_solar_position_skyfield(
longitude=longitude,
latitude=latitude,
timestamp=timestamp,
)
solar_altitude, solar_azimuth, distance_to_sun = solar_position.altaz()
solar_altitude = SolarAltitude(
value=solar_altitude.radians,
unit=RADIANS,
solar_positioning_algorithm="Skyfield",
solar_timing_algorithm="Skyfield",
)
solar_azimuth = SolarAzimuth(
value=solar_azimuth.radians,
unit=RADIANS,
solar_positioning_algorithm="Skyfield",
solar_timing_algorithm="Skyfield",
)
if (
not isfinite(solar_azimuth.degrees)
or not solar_azimuth.min_degrees
<= solar_azimuth.degrees
<= solar_azimuth.max_degrees
):
raise ValueError(
f"The calculated solar azimuth angle {solar_azimuth.degrees} is out of the expected range\
[{solar_azimuth.min_degrees}, {solar_azimuth.max_degrees}] degrees"
)
if (
not isfinite(solar_altitude.degrees)
or not solar_altitude.min_degrees
<= solar_altitude.degrees
<= solar_altitude.max_degrees
):
raise ValueError(
f"The calculated solar altitude angle {solar_altitude.degrees} is out of the expected range\
[{solar_altitude.min_degrees}, {solar_altitude.max_degrees}] degrees"
)
return solar_altitude, solar_azimuth # , distance_to_sun
calculate_solar_hour_angle_declination_skyfield ¶
calculate_solar_hour_angle_declination_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
timezone: str = None,
) -> Tuple[SolarHourAngle, SolarDeclination]
Calculate the hour angle ω'
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Returns | | required | |
hour_angle | Hour angle is the angle (ω) at any instant through which the earth has to turn to bring the meridian of the observer directly in line with the sun's rays measured in radian. | required |
Source code in pvgisprototype/algorithms/skyfield/solar_geometry.py
@validate_with_pydantic(SolarHourAngleSkyfieldInput)
def calculate_solar_hour_angle_declination_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
timezone: str = None,
) -> Tuple[SolarHourAngle, SolarDeclination]:
"""Calculate the hour angle ω'
Parameters
----------
Returns
--------
hour_angle: float
Hour angle is the angle (ω) at any instant through which the earth has
to turn to bring the meridian of the observer directly in line with the
sun's rays measured in radian.
"""
solar_position = calculate_solar_position_skyfield(
longitude=longitude,
latitude=latitude,
timestamp=timestamp,
timezone=timezone,
)
hour_angle, solar_declination, distance_to_sun = solar_position.hadec()
hour_angle = SolarHourAngle(
value=hour_angle.radians,
unit=RADIANS,
solar_positioning_algorithm="Skyfield",
solar_timing_algorithm="Skyfield",
)
solar_declination = SolarDeclination(
value=solar_declination.radians,
unit=RADIANS,
solar_positioning_algorithm="Skyfield",
solar_timing_algorithm="Skyfield",
)
if (
not isfinite(hour_angle.degrees)
or not hour_angle.min_degrees <= hour_angle.degrees <= hour_angle.max_degrees
):
raise ValueError(
f"The calculated solar hour angle {hour_angle.degrees} is out of the expected range\
[{hour_angle.min_degrees}, {hour_angle.max_degrees}] degrees"
)
if (
not isfinite(solar_declination.degrees)
or not solar_declination.min_degrees
<= solar_declination.degrees
<= solar_declination.max_degrees
):
raise ValueError(
f"The calculated solar declination angle {solar_declination.degrees} is out of the expected range\
[{solar_declination.min_degrees}, {solar_declination.max_degrees}] degrees"
)
return hour_angle, solar_declination
calculate_solar_position_skyfield ¶
calculate_solar_position_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
)
Calculate sun position above the local horizon using Skyfield.
Returns:
| Name | Type | Description |
|---|---|---|
solar_altitude | Altitude measures the angle above or below the horizon. The zenith is at +90°, an object on the horizon’s great circle is at 0°, and the nadir beneath your feet is at −90°. | |
solar_azimuth | Azimuth measures the angle around the sky from the north pole: 0° means exactly north, 90° is east, 180° is south, and 270° is west. |
Notes
For the implementation, see also: https://techoverflow.net/2022/06/19/how-to-compute-position-of-sun-in-the-sky-in-python-using-skyfield/
Factors influencing the accuracy of the calculation using Skyfield:
-
Skyfield considers:
- The slight shift in position caused by light speed
- The very very slight shift in position caused by earth’s gravity
-
Skyfield does not consider:
- Atmospheric distortions shifting the sun’s position
- The extent of the sun’s disk causing the sun to emanate not from a point but apparently from an area
Notes
To consider:
- https://rhodesmill.org/skyfield/almanac.html#elevated-vantage-points
- The
Timeclass in Skyfield (i.e.timescale()) only uses UTC for input and output - https://rhodesmill.org/skyfield/time.html#utc-and-your-timezone
- https://rhodesmill.org/skyfield/time.html#utc-and-leap-seconds
Source code in pvgisprototype/algorithms/skyfield/solar_geometry.py
@validate_with_pydantic(CalculateSolarPositionSkyfieldInputModel)
def calculate_solar_position_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
):
"""Calculate sun position above the local horizon using Skyfield.
Returns
-------
solar_altitude:
Altitude measures the angle above or below the horizon. The
zenith is at +90°, an object on the horizon’s great circle is at 0°,
and the nadir beneath your feet is at −90°.
solar_azimuth:
Azimuth measures the angle around the sky from the north pole: 0° means
exactly north, 90° is east, 180° is south, and 270° is west.
Notes
-----
For the implementation, see also:
https://techoverflow.net/2022/06/19/how-to-compute-position-of-sun-in-the-sky-in-python-using-skyfield/
Factors influencing the accuracy of the calculation using Skyfield:
- Skyfield considers:
- The slight shift in position caused by light speed
- The very very slight shift in position caused by earth’s gravity
- Skyfield does not consider:
- Atmospheric distortions shifting the sun’s position
- The extent of the sun’s disk causing the sun to emanate not from a
point but apparently from an area
Notes
-----
To consider:
- https://rhodesmill.org/skyfield/almanac.html#elevated-vantage-points
- The `Time` class in Skyfield (i.e. `timescale()`) only uses UTC for input
and output
- https://rhodesmill.org/skyfield/time.html#utc-and-your-timezone
- https://rhodesmill.org/skyfield/time.html#utc-and-leap-seconds
"""
# # Handle Me during input validation? -------------------------------------
# try:
# timestamp = timezone.localize(timestamp)
# except Exception:
# logging.warning(f'tzinfo already set for timestamp = {timestamp}')
# # Handle Me during input validation? -------------------------------------
planets = load("de421.bsp")
sun = planets["Sun"]
earth = planets["Earth"]
location = wgs84.latlon(latitude.degrees, longitude.degrees)
timescale = load.timescale()
requested_timestamp = timescale.from_datetime(timestamp)
# sun position seen from observer location
# sun position seen from observer location
solar_position = (earth + location).at(requested_timestamp).observe(sun).apparent()
return solar_position
solar_time ¶
Functions:
| Name | Description |
|---|---|
calculate_solar_time_skyfield | Calculate the solar time using Skyfield |
calculate_solar_time_skyfield ¶
calculate_solar_time_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
timezone: ZoneInfo,
verbose: int = VERBOSE_LEVEL_DEFAULT,
) -> Timestamp
Calculate the solar time using Skyfield
The position of the Sun in the sky changes slightly day to day due to the Earth's elliptical orbit and axis tilt around the Sun. In consequence, the length of each solar day varies slightly.
The mean solar time (as "opposed" to the apparent solar time which is measured with a sundial), averages out these variations to create a "mean" or average solar day of 24 hours long. This is the basis of our standard time system, although further adjusted to create the time zones.
A key concept in solar time is the solar noon, the moment when the Sun reaches its highest point in the sky each day. In apparent solar time, solar noon is the moment the Sun crosses the local meridian (an imaginary line that runs from North to South overhead).
Solar time can vary significantly from standard clock time, depending on the time of year and the observer's longitude within their time zone. For example, at the eastern edge of a time zone, the Sun may reach its highest point (solar noon) significantly earlier than noon according to standard clock time.
Source code in pvgisprototype/algorithms/skyfield/solar_time.py
@validate_with_pydantic(CalculateSolarTimeSkyfieldInputModel)
def calculate_solar_time_skyfield(
longitude: Longitude,
latitude: Latitude,
timestamp: Timestamp,
timezone: ZoneInfo,
verbose: int = VERBOSE_LEVEL_DEFAULT,
) -> Timestamp:
"""Calculate the solar time using Skyfield
The position of the Sun in the sky changes slightly day to day due to the
Earth's elliptical orbit and axis tilt around the Sun. In consequence, the
length of each solar day varies slightly.
The mean solar time (as "opposed" to the apparent solar time which is
measured with a sundial), averages out these variations to create a "mean"
or average solar day of 24 hours long. This is the basis of our standard
time system, although further adjusted to create the time zones.
A key concept in solar time is the solar noon, the moment when the Sun
reaches its highest point in the sky each day. In apparent solar time,
solar noon is the moment the Sun crosses the local meridian (an imaginary
line that runs from North to South overhead).
Solar time can vary significantly from standard clock time, depending on
the time of year and the observer's longitude within their time zone. For
example, at the eastern edge of a time zone, the Sun may reach its highest
point (solar noon) significantly earlier than noon according to standard
clock time.
"""
# Handle Me during input validation? -------------------------------------
if timezone != timestamp.tzinfo:
try:
timestamp = timestamp.astimezone(timezone)
except Exception as e:
logger.warning(f"Error setting tzinfo for timestamp = {timestamp}: {e}")
# Handle Me during input validation? -------------------------------------
midnight = timestamp.replace(hour=0, minute=0, second=0, microsecond=0)
next_midnight = midnight + Timedelta(days=1)
timescale = load.timescale()
midnight_time = timescale.from_datetime(midnight)
next_midnight_time = timescale.from_datetime(next_midnight)
planets = load("de421.bsp")
sun = planets["Sun"]
if longitude.degrees > 0:
location = wgs84.latlon(latitude.degrees * N, longitude.degrees * E)
if longitude.degrees < 0:
location = wgs84.latlon(
latitude.degrees * N, longitude.degrees * W
) # Correct ?
if longitude.degrees > 0:
location = wgs84.latlon(latitude.degrees * N, longitude.degrees * E)
if longitude.degrees < 0:
location = wgs84.latlon(
latitude.degrees * N, longitude.degrees * W
) # Correct ?
else:
location = wgs84.latlon(latitude.degrees * N, longitude.degrees) # Correct ?
location = wgs84.latlon(latitude.degrees * N, longitude.degrees) # Correct ?
f = almanac.meridian_transits(planets, sun, location)
times, events = almanac.find_discrete(midnight_time, next_midnight_time, f)
times = times[events == 1] # select transits instead of antitransits
if not times:
raise ValueError("No solar noon found in the given time range")
next_solar_noon = times[0] # first in `times` is the _next_ solar noon!
previous_solar_noon = next_solar_noon - Timedelta(days=1)
if timestamp < next_solar_noon.utc_datetime().replace(tzinfo=ZoneInfo("UTC")):
# if morning : calculate hours until next solar noon
hours_since_solar_noon = (
timestamp
- previous_solar_noon.utc_datetime().replace(tzinfo=ZoneInfo("UTC"))
).total_seconds() / 3600
else:
# if afternoon : calculate hours since last solar noon
hours_since_solar_noon = (
timestamp - next_solar_noon.utc_datetime().replace(tzinfo=ZoneInfo("UTC"))
).total_seconds() / 3600
hours = int(hours_since_solar_noon)
minutes = int((hours_since_solar_noon - hours) * 60)
seconds = int(((hours_since_solar_noon - hours) * 60 - minutes) * 60)
local_solar_time = Timestamp( # NOTE gounaol: Maybe wrong implementation
year=timestamp.year,
month=timestamp.month,
day=timestamp.day,
hour=int(hours),
minute=int(minutes),
second=int(seconds),
tzinfo=timestamp.tzinfo,
)
# local_solar_time = previous_solar_noon.utc_datetime() + Timedelta(hours=hours_since_solar_noon)
if verbose:
print(f"Local solar time: {local_solar_time}")
previous_solar_noon_string = previous_solar_noon.astimezone(timezone).strftime(
"%Y-%m-%d %H:%M:%S"
)
print(f"Previous solar noon: {previous_solar_noon_string}")
next_solar_noon_string = next_solar_noon.astimezone(timezone).strftime(
"%Y-%m-%d %H:%M:%S"
)
print(f"Next solar noon: {next_solar_noon_string}")
return local_solar_time
sunrise_and_sunset ¶
usno ¶
Modules:
| Name | Description |
|---|---|
local_hour_angle | |
sidereal_time | |
solar_altitude | |
solar_azimuth | |
local_hour_angle ¶
Functions:
| Name | Description |
|---|---|
calculate_local_hour_angle | Calculate the Local Hour Angle (local_hour_angle) |
calculate_local_hour_angle ¶
Calculate the Local Hour Angle (local_hour_angle)
Examples:
gast_array = np.array([15.0116663, 16.08011407]) alpha_array = np.array([14.261, 14.261]) # Example right ascension in hours longitude_array = np.array([77.5946, 77.5946]) # Example longitude in degrees (east) delta_array = np.array([23.44, 23.44]) # Example declination in degrees latitude_array = np.array([12.9714, 12.9714]) # Example latitude in degrees local_hour_angle_array = calculate_local_hour_angle(gast_array, alpha_array, longitude_array)
Source code in pvgisprototype/algorithms/usno/local_hour_angle.py
def calculate_local_hour_angle(gast, alpha, longitude):
"""Calculate the Local Hour Angle (local_hour_angle)
Examples
--------
gast_array = np.array([15.0116663, 16.08011407])
alpha_array = np.array([14.261, 14.261]) # Example right ascension in hours
longitude_array = np.array([77.5946, 77.5946]) # Example longitude in degrees (east)
delta_array = np.array([23.44, 23.44]) # Example declination in degrees
latitude_array = np.array([12.9714, 12.9714]) # Example latitude in degrees
local_hour_angle_array = calculate_local_hour_angle(gast_array, alpha_array, longitude_array)
"""
return (gast - alpha) * 15 + longitude
sidereal_time ¶
Functions:
| Name | Description |
|---|---|
calculate_apparent_sidereal_time | Calculate the apparent sidereal time (GAST) |
calculate_apparent_sidereal_time_time_series | Calculate the apparent sidereal time |
calculate_eqeq | Compute the equation of the equinoxes (eqeq) |
calculate_eqeq_time_series | Calculate the equation of the equinoxes (eqeq) |
calculate_gast | Compute the Greenwich apparent sidereal time (GAST) |
calculate_gast_time_series | Calculate the Greenwich apparent sidereal time (GAST) |
calculate_gmst | Compute the Greenwich mean sidereal time (GMST) |
calculate_gmst_time_series | Calculate the Greenwich mean sidereal time (GMST) using NumPy |
calculate_local_sidereal_time | Compute the local mean or apparent sidereal time |
calculate_local_sidereal_time_time_series | Calculate the local mean or apparent sidereal time |
calculate_apparent_sidereal_time ¶
calculate_apparent_sidereal_time(
julian_date_universal_time,
H,
local_longitude_deg,
use_precise_formula=True,
)
Calculate the apparent sidereal time (GAST)
Source code in pvgisprototype/algorithms/usno/sidereal_time.py
def calculate_apparent_sidereal_time(
julian_date_universal_time, H, local_longitude_deg, use_precise_formula=True
):
"""Calculate the apparent sidereal time (GAST)"""
GMST = calculate_GMST(julian_date_universal_time, H, use_precise_formula)
DTT = julian_date_universal_time - 2451545.0
eqeq = calculate_eqeq(DTT)
GAST = calculate_GAST(GMST, eqeq)
local_sidereal_time = calculate_local_sidereal_time(GAST, local_longitude_deg)
return {
"GMST": GMST,
"eqeq": eqeq,
"GAST": GAST,
"local_sidereal_time": local_sidereal_time,
}
calculate_apparent_sidereal_time_time_series ¶
calculate_apparent_sidereal_time_time_series(
julian_date_universal_time,
H,
local_longitude_deg,
use_precise_formula=True,
)
Calculate the apparent sidereal time
Source code in pvgisprototype/algorithms/usno/sidereal_time.py
def calculate_apparent_sidereal_time_time_series(
julian_date_universal_time, H, local_longitude_deg, use_precise_formula=True
):
"""Calculate the apparent sidereal time"""
GMST = calculate_GMST_time_series(
julian_date_universal_time, H, use_precise_formula
)
DTT = julian_date_universal_time - 2451545.0
eqeq = calculate_eqeq_time_series(DTT)
GAST = calculate_GAST_time_series(GMST, eqeq)
local_sidereal_time = calculate_local_sidereal_time_time_series(
GAST, local_longitude_deg
)
return {
"GMST": GMST,
"eqeq": eqeq,
"GAST": GAST,
"local_sidereal_time": local_sidereal_time,
}
calculate_eqeq ¶
Compute the equation of the equinoxes (eqeq)
Source code in pvgisprototype/algorithms/usno/sidereal_time.py
def calculate_eqeq(DTT):
"""Compute the equation of the equinoxes (eqeq)"""
Omega = 125.04 - 0.052954 * DTT
L = 280.47 + 0.98565 * DTT
epsilon = 23.4393 - 0.0000004 * DTT
delta_psi = -0.000319 * sin(radians(Omega)) - 0.000024 * sin(radians(2 * L))
eqeq = delta_psi * cos(radians(epsilon))
return eqeq
calculate_eqeq_time_series ¶
Calculate the equation of the equinoxes (eqeq)
Source code in pvgisprototype/algorithms/usno/sidereal_time.py
def calculate_eqeq_time_series(DTT):
"""Calculate the equation of the equinoxes (eqeq)"""
Omega = 125.04 - 0.052954 * DTT
L = 280.47 + 0.98565 * DTT
epsilon = 23.4393 - 0.0000004 * DTT
delta_psi = -0.000319 * np.sin(np.radians(Omega)) - 0.000024 * np.sin(
np.radians(2 * L)
)
eqeq = delta_psi * np.cos(np.radians(epsilon))
return eqeq
calculate_gast ¶
calculate_gast_time_series ¶
calculate_gmst ¶
Compute the Greenwich mean sidereal time (GMST)
Source code in pvgisprototype/algorithms/usno/sidereal_time.py
def calculate_gmst(julian_date_universal_time, H, use_precise_formula=True):
"""Compute the Greenwich mean sidereal time (GMST)"""
difference_universal_time_minus_20000101120000 = (
julian_date_universal_time - 2451545.0
)
T = difference_universal_time_minus_20000101120000 / 36525.0
if use_precise_formula:
GMST = fmod(
6.697375
+ 0.065709824279 * difference_universal_time_minus_20000101120000
+ 1.0027379 * H
+ 0.0000258 * T**2,
24,
)
else:
GMST = fmod(
18.697375
+ 24.065709824279 * difference_universal_time_minus_20000101120000,
24,
)
return GMST
calculate_gmst_time_series ¶
Calculate the Greenwich mean sidereal time (GMST) using NumPy
Source code in pvgisprototype/algorithms/usno/sidereal_time.py
def calculate_gmst_time_series(julian_date_universal_time, H, use_precise_formula=True):
"""Calculate the Greenwich mean sidereal time (GMST) using NumPy"""
difference_universal_time_minus_20000101120000 = (
julian_date_universal_time - 2451545.0
)
centuries_since_2000 = difference_universal_time_minus_20000101120000 / 36525.0
if use_precise_formula:
GMST = np.mod(
6.697375
+ 0.065709824279 * difference_universal_time_minus_20000101120000
+ 1.0027379 * H
+ 0.0000258 * centuries_since_2000**2,
24,
)
else:
GMST = np.mod(
18.697375
+ 24.065709824279 * difference_universal_time_minus_20000101120000,
24,
)
return GMST
calculate_local_sidereal_time ¶
Compute the local mean or apparent sidereal time
calculate_local_sidereal_time_time_series ¶
Calculate the local mean or apparent sidereal time
solar_altitude ¶
Functions:
| Name | Description |
|---|---|
compute_altitude | Function to calculate the Altitude (a) |
compute_altitude ¶
Function to calculate the Altitude (a)
Examples:
GAST_array = np.array([15.0116663, 16.08011407]) alpha_array = np.array([14.261, 14.261]) # Example right ascension in hours longitude_array = np.array([77.5946, 77.5946]) # Example longitude in degrees (east) delta_array = np.array([23.44, 23.44]) # Example declination in degrees latitude_array = np.array([12.9714, 12.9714]) # Example latitude in degrees LHA_array = compute_LHA(GAST_array, alpha_array, longitude_array) azimuth_array = compute_azimuth(LHA_array, delta_array, latitude_array)
Source code in pvgisprototype/algorithms/usno/solar_altitude.py
def compute_altitude(LHA, delta, latitude):
"""Function to calculate the Altitude (a)
Examples
--------
GAST_array = np.array([15.0116663, 16.08011407])
alpha_array = np.array([14.261, 14.261]) # Example right ascension in hours
longitude_array = np.array([77.5946, 77.5946]) # Example longitude in degrees (east)
delta_array = np.array([23.44, 23.44]) # Example declination in degrees
latitude_array = np.array([12.9714, 12.9714]) # Example latitude in degrees
LHA_array = compute_LHA(GAST_array, alpha_array, longitude_array)
azimuth_array = compute_azimuth(LHA_array, delta_array, latitude_array)
"""
LHA_rad = np.radians(LHA)
delta_rad = np.radians(delta)
latitude_rad = np.radians(latitude)
sin_altitude = np.cos(LHA_rad) * np.cos(delta_rad) * np.cos(latitude_rad) + np.sin(
delta_rad
) * np.sin(latitude_rad)
return np.degrees(np.arcsin(sin_altitude))
solar_azimuth ¶
Functions:
| Name | Description |
|---|---|
calculate_azimuth | Calculate the Azimuth (A) |
calculate_azimuth ¶
Calculate the Azimuth (A)
Examples:
azimuth_array = calculate_azimuth(local_hour_angle_array, delta_array, latitude_array)
Source code in pvgisprototype/algorithms/usno/solar_azimuth.py
def calculate_azimuth(local_hour_angle, delta, latitude):
"""Calculate the Azimuth (A)
Examples
--------
azimuth_array = calculate_azimuth(local_hour_angle_array, delta_array, latitude_array)
"""
local_hour_angle_rad = np.radians(local_hour_angle)
delta_rad = np.radians(delta)
latitude_rad = np.radians(latitude)
tan_azimuth = -np.sin(local_hour_angle_rad) / (
np.tan(delta_rad) * np.cos(latitude_rad)
- np.sin(latitude_rad) * np.cos(local_hour_angle_rad)
)
azimuth = np.degrees(
np.arctan2(
-np.sin(local_hour_angle_rad),
np.tan(delta_rad) * np.cos(latitude_rad)
- np.sin(latitude_rad) * np.cos(local_hour_angle_rad),
)
)
return np.mod(azimuth + 360, 360) # Normalize to [0, 360)