PVGIS features an algorithmic repository, independent from the core API, the CLI and the Web API components.
algorithms
¶
solar_declination
¶
Functions:
| Name | Description |
|---|---|
calculate_solar_declination_hargreaves |
Approximate the solar declination based on the Hargreaves formula. |
calculate_solar_declination_hargreaves
¶
calculate_solar_declination_hargreaves(
timestamp: Timestamp = partial(now(tz=TIMEZONE_UTC)),
) -> SolarDeclination
Approximate the solar declination based on the Hargreaves formula.
⎛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 pvgis/algorithms/hargreaves/solar_declination.py
@validate_with_pydantic(CalculateSolarDeclinationHargreavesInputModel)
def calculate_solar_declination_hargreaves(
timestamp: Timestamp = partial(Timestamp.now(tz=TIMEZONE_UTC)),
) -> SolarDeclination:
"""Approximate the solar declination based on the Hargreaves formula.
⎛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.
"""
days_in_year = get_days_in_year(timestamp.year)
day_of_year = timestamp.timetuple().tm_yday
declination_value_in_degrees = 23.45 * sin(
radians(
360
/ days_in_year
* (
284
+ day_of_year
+ 0.4 * sin(radians(360 / days_in_year * (day_of_year - 100)))
)
)
)
solar_declination = SolarDeclination(
value=declination_value_in_degrees,
unit=DEGREES,
position_algorithm="Hargreaves",
timing_algorithm="Hargreaves",
)
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 solar_declination
jenco
¶
Modules:
| Name | Description |
|---|---|
solar_altitude |
|
solar_azimuth |
|
solar_declination |
|
solar_incidence |
|
milne1921
¶
Modules:
| Name | Description |
|---|---|
solar_time |
|
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. |
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.
function_models
¶
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 pvgis/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,
position_algorithm=SolarPositionModel.noaa,
)
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 pvgis/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,
)
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 pvgis/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,
position_algorithm="NOAA",
timing_algorithm="NOAA",
)
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 pvgis/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,
)
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,
refracted_solar_zenith: RefractedSolarZenith,
apply_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:
| 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 |
refracted_solar_zenith
|
RefractedSolarZenith
|
Refracted solar zenith angle in radians. |
required |
apply_atmospheric_refraction
|
bool
|
Whether to apply atmospheric refraction corrections (default is False). |
False
|
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 pvgis/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,
refracted_solar_zenith: RefractedSolarZenith, # radians
apply_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.
refracted_solar_zenith : RefractedSolarZenith
Refracted solar zenith angle in radians.
apply_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,
event="noon",
refracted_solar_zenith=refracted_solar_zenith,
apply_atmospheric_refraction=apply_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
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.
Returns:
| Type | Description |
|---|---|
SolarHourAngle
|
Time series of solar hour angles in radians. |
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 pvgis/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.
Returns
-------
SolarHourAngle
Time series of solar hour angles in radians.
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,
position_algorithm=SolarPositionModel.noaa,
timing_algorithm=true_solar_time_series.timing_algorithm,
)
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,
refracted_solar_zenith: RefractedSolarZenith,
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
|
Geographic latitude for the calculation. |
required |
timestamps
|
DatetimeIndex
|
Timestamps aligned with |
required |
refracted_solar_zenith
|
RefractedSolarZenith
|
Refracted solar zenith series (radians). NOAA uses ~90.833° at sunrise/set including refraction and solar disk size. |
required |
dtype
|
str
|
Array dtype. |
DATA_TYPE_DEFAULT
|
array_backend
|
str
|
Array backend identifier. |
ARRAY_BACKEND_DEFAULT
|
verbose
|
int
|
Verbosity level. |
VERBOSE_LEVEL_DEFAULT
|
log
|
int
|
Logging level. |
LOG_LEVEL_DEFAULT
|
Returns:
| Type | Description |
|---|---|
EventHourAngle
|
Event hour angle time series in radians. |
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 pvgis/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,
refracted_solar_zenith: RefractedSolarZenith,
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
Geographic latitude for the calculation.
timestamps : DatetimeIndex
Timestamps aligned with ``refracted_solar_zenith``.
refracted_solar_zenith : RefractedSolarZenith
Refracted solar zenith series (radians). NOAA uses ~90.833° at sunrise/set
including refraction and solar disk size.
dtype : str, optional
Array dtype.
array_backend : str, optional
Array backend identifier.
verbose : int, optional
Verbosity level.
log : int, optional
Logging level.
Returns
-------
EventHourAngle
Event hour angle time series in radians.
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(refracted_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, dtype=dtype)
) # 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 pvgis/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.
"""
# Use float64 for intermediate arithmetic to reduce timestamp drift while
# preserving public model dtype behavior elsewhere.
equation_of_time_minutes = numpy.asarray(
equation_of_time.minutes, dtype=numpy.float64
)
longitude_minutes = numpy.asarray(longitude.as_minutes, dtype=numpy.float64)
event_hour_angle_minutes = numpy.asarray(
event_hour_angle.as_minutes, dtype=numpy.float64
)
base = 720.0 - equation_of_time_minutes + float(timezone_offset_hours_utc) * 60.0
# 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}")
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],
refracted_solar_zenith: RefractedSolarZenith = REFRACTED_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 |
refracted_solar_zenith
|
float
|
The refracted solar zenith angle in radians. Default is the cosine of 90.833 degrees. |
REFRACTED_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 pvgis/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],
refracted_solar_zenith: RefractedSolarZenith = REFRACTED_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
----------
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.
refracted_solar_zenith : float, optional
The refracted solar zenith angle in radians. Default is the cosine of 90.833 degrees.
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 == [None]:
return EventTime(
value=None,
event=None,
)
# Keep one representative timestamp per day while preserving its original
# time-of-day. Using normalized midnight timestamps can shift declination
# and equation-of-time enough to miss strict second-level test tolerances.
unique_days = DatetimeIndex(
timestamps.to_series(index=timestamps)
.groupby(timestamps.normalize())
.first()
.to_numpy()
)
event_hour_angle_series = calculate_event_hour_angle_series_noaa(
latitude=latitude,
timestamps=unique_days,
refracted_solar_zenith=refracted_solar_zenith,
dtype=dtype,
)
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 pvgis.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=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 pvgis/algorithms/noaa/event_time.py
def match_event_times_to_timestamps(
solar_event_series: dict,
timestamps: DatetimeIndex,
array_backend: str,
):
""" """
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")
# Assign event to nearest timestamp index.
for idx, event_time in zip(event_indexer, event_times):
if idx >= 0: # Valid index
event_timestamps[idx] = event_time.to_numpy()
event_types[idx] = (
event_type.name if hasattr(event_type, "name") else str(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/
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 pvgis/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)
)
if validate_output:
if not np.all(
(SolarDeclination().min_radians <= solar_declination_series)
& (solar_declination_series <= SolarDeclination().max_radians)
):
index_of_out_of_range_values = np.where(
(solar_declination_series < SolarDeclination().min_radians)
| (solar_declination_series > SolarDeclination().max_radians)
)
out_of_range_values = solar_declination_series[index_of_out_of_range_values]
raise ValueError(
f"{WARNING_OUT_OF_RANGE_VALUES} "
f"[{SolarDeclination().min_degrees}, {SolarDeclination().max_degrees}] degrees"
f" in [code]solar_declination_series[/code] : {np.degrees(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,
position_algorithm=fractional_year_series.position_algorithm,
)
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 pvgis/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,
position_algorithm=solar_zenith_series.position_algorithm,
timing_algorithm=solar_zenith_series.timing_algorithm,
)
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 pvgis/algorithms/noaa/solar_zenith.py
def atmospheric_refraction_adjustment(
solar_altitude: np.ndarray, dtype: str = "float64" # radians
) -> 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,
apply_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 pvgis/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,
apply_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
)
# NOTE Ensure values are within [1, 1]
cosine_solar_zenith = np.clip(cosine_solar_zenith, -1, 1)
solar_zenith_series = SolarZenith(
value=np.arccos(cosine_solar_zenith), # Important !
unit=RADIANS,
position_algorithm=solar_declination_series.position_algorithm,
timing_algorithm=solar_hour_angle_series.timing_algorithm,
)
if apply_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"
)
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 # This is a SolarZenith data class !
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,
apply_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 pvgis/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,
apply_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,
apply_atmospheric_refraction=apply_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
if validate_output:
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,
position_algorithm=solar_zenith_series.position_algorithm,
timing_algorithm=solar_zenith_series.timing_algorithm,
)
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,
apply_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 |
apply_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], pysolar [3], Skyfield [4]_ and suncalc [5]_, 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
.. [3] https://github.com/pingswept/pysolar
Examples:
>>> from math import radians
>>> from pvgis.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 pvgis.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"),
... apply_atmospheric_refraction=True
... )
>>> print(solar_azimuth_series)
value=array([0.20324741, 0.681624 , 1.0347459 , 1.295689 , 1.505737 ,
1.691547 , 1.8700209 , 2.06125 , 2.250216 , 2.466558 ,
2.708641 , 2.9751856 , 3.2346206 , 3.50884 , 3.7576592 ,
3.9807858 , 4.1786404 , 4.3685203 , 4.547683 , 4.73073 ,
4.933111 , 5.178298 , 5.5035386 , 5.9491134 , 0.19983122],
dtype=float32) unit='radians' position_algorithm='NOAA' timing_algorithm='NOAA' min_radians=0 max_radians=6.283185307179586 min_degrees=0 max_degrees=360
>>> print(solar_azimuth_series.degrees)
[ 11.645218 39.054176 59.28657 74.2375 86.27237 96.9185
107.144295 118.100914 128.92787 141.32335 155.1937 170.46558
185.3301 201.04172 215.298 228.08221 239.41844 250.29776
260.56302 271.05084 282.64642 296.6946 315.32953 340.85907
11.449485]
Source code in pvgis/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,
apply_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.
apply_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]_,
pysolar [3]_, Skyfield [4]_ and suncalc [5]_, 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/pingswept/pysolar
.. [4] https://github.com/skyfielders/python-skyfield/
.. [5] https://github.com/kylebarron/suncalc-py
Examples
--------
>>> from math import radians
>>> from pvgis.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 pvgis.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"),
... apply_atmospheric_refraction=True
... )
>>> print(solar_azimuth_series)
value=array([0.20324741, 0.681624 , 1.0347459 , 1.295689 , 1.505737 ,
1.691547 , 1.8700209 , 2.06125 , 2.250216 , 2.466558 ,
2.708641 , 2.9751856 , 3.2346206 , 3.50884 , 3.7576592 ,
3.9807858 , 4.1786404 , 4.3685203 , 4.547683 , 4.73073 ,
4.933111 , 5.178298 , 5.5035386 , 5.9491134 , 0.19983122],
dtype=float32) unit='radians' position_algorithm='NOAA' timing_algorithm='NOAA' min_radians=0 max_radians=6.283185307179586 min_degrees=0 max_degrees=360
>>> print(solar_azimuth_series.degrees)
[ 11.645218 39.054176 59.28657 74.2375 86.27237 96.9185
107.144295 118.100914 128.92787 141.32335 155.1937 170.46558
185.3301 201.04172 215.298 228.08221 239.41844 250.29776
260.56302 271.05084 282.64642 296.6946 315.32953 340.85907
11.449485]
"""
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,
apply_atmospheric_refraction=apply_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,
position_algorithm=SolarPositionModel.noaa,
timing_algorithm=SolarTimeModel.noaa,
origin="North",
)
pvgis
¶
Modules:
| Name | Description |
|---|---|
average_photon_energy |
|
direct |
|
extraterrestrial |
|
fractional_year |
|
ground_reflected |
|
power |
|
read |
|
shading |
|
solar_altitude |
|
solar_azimuth |
|
solar_declination |
|
solar_hour_angle |
|
solar_incidence |
|
spectral_factor |
|