You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1269 lines
37 KiB
1269 lines
37 KiB
import datetime
|
|
from math import acos, asin, atan2, cos, degrees, fabs, radians, sin, sqrt, tan
|
|
from typing import Dict, Optional, Tuple, Union
|
|
|
|
try:
|
|
import zoneinfo
|
|
except ImportError:
|
|
from backports import zoneinfo
|
|
|
|
from astral import (
|
|
Depression,
|
|
Minutes,
|
|
Observer,
|
|
SunDirection,
|
|
TimePeriod,
|
|
now,
|
|
refraction_at_zenith,
|
|
today,
|
|
)
|
|
from astral.julian import julianday, julianday_to_juliancentury
|
|
|
|
__all__ = [
|
|
"sun",
|
|
"dawn",
|
|
"sunrise",
|
|
"noon",
|
|
"midnight",
|
|
"sunset",
|
|
"dusk",
|
|
"daylight",
|
|
"night",
|
|
"twilight",
|
|
"blue_hour",
|
|
"golden_hour",
|
|
"rahukaalam",
|
|
"zenith",
|
|
"azimuth",
|
|
"elevation",
|
|
"time_at_elevation",
|
|
]
|
|
|
|
|
|
# Using 32 arc minutes as sun's apparent diameter
|
|
SUN_APPARENT_RADIUS = 32.0 / (60.0 * 2.0)
|
|
|
|
|
|
# region Backend
|
|
def minutes_to_timedelta(minutes: float) -> datetime.timedelta:
|
|
"""Convert a floating point number of minutes to a
|
|
:class:`~datetime.timedelta`
|
|
"""
|
|
d = int(minutes / 1440)
|
|
minutes = minutes - (d * 1440)
|
|
minutes = minutes * 60
|
|
s = int(minutes)
|
|
sfrac = minutes - s
|
|
us = int(sfrac * 1_000_000)
|
|
|
|
return datetime.timedelta(days=d, seconds=s, microseconds=us)
|
|
|
|
|
|
def geom_mean_long_sun(juliancentury: float) -> float:
|
|
"""Calculate the geometric mean longitude of the sun"""
|
|
l0 = 280.46646 + juliancentury * (36000.76983 + 0.0003032 * juliancentury)
|
|
return l0 % 360.0
|
|
|
|
|
|
def geom_mean_anomaly_sun(juliancentury: float) -> float:
|
|
"""Calculate the geometric mean anomaly of the sun"""
|
|
return 357.52911 + juliancentury * (35999.05029 - 0.0001537 * juliancentury)
|
|
|
|
|
|
def eccentric_location_earth_orbit(juliancentury: float) -> float:
|
|
"""Calculate the eccentricity of Earth's orbit"""
|
|
return 0.016708634 - juliancentury * (0.000042037 + 0.0000001267 * juliancentury)
|
|
|
|
|
|
def sun_eq_of_center(juliancentury: float) -> float:
|
|
"""Calculate the equation of the center of the sun"""
|
|
m = geom_mean_anomaly_sun(juliancentury)
|
|
|
|
mrad = radians(m)
|
|
sinm = sin(mrad)
|
|
sin2m = sin(mrad + mrad)
|
|
sin3m = sin(mrad + mrad + mrad)
|
|
|
|
c = (
|
|
sinm * (1.914602 - juliancentury * (0.004817 + 0.000014 * juliancentury))
|
|
+ sin2m * (0.019993 - 0.000101 * juliancentury)
|
|
+ sin3m * 0.000289
|
|
)
|
|
|
|
return c
|
|
|
|
|
|
def sun_true_long(juliancentury: float) -> float:
|
|
"""Calculate the sun's true longitude"""
|
|
l0 = geom_mean_long_sun(juliancentury)
|
|
c = sun_eq_of_center(juliancentury)
|
|
|
|
return l0 + c
|
|
|
|
|
|
def sun_true_anomoly(juliancentury: float) -> float:
|
|
"""Calculate the sun's true anomaly"""
|
|
m = geom_mean_anomaly_sun(juliancentury)
|
|
c = sun_eq_of_center(juliancentury)
|
|
|
|
return m + c
|
|
|
|
|
|
def sun_rad_vector(juliancentury: float) -> float:
|
|
v = sun_true_anomoly(juliancentury)
|
|
e = eccentric_location_earth_orbit(juliancentury)
|
|
|
|
return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v)))
|
|
|
|
|
|
def sun_apparent_long(juliancentury: float) -> float:
|
|
true_long = sun_true_long(juliancentury)
|
|
|
|
omega = 125.04 - 1934.136 * juliancentury
|
|
return true_long - 0.00569 - 0.00478 * sin(radians(omega))
|
|
|
|
|
|
def mean_obliquity_of_ecliptic(juliancentury: float) -> float:
|
|
seconds = 21.448 - juliancentury * (
|
|
46.815 + juliancentury * (0.00059 - juliancentury * (0.001813))
|
|
)
|
|
return 23.0 + (26.0 + (seconds / 60.0)) / 60.0
|
|
|
|
|
|
def obliquity_correction(juliancentury: float) -> float:
|
|
e0 = mean_obliquity_of_ecliptic(juliancentury)
|
|
|
|
omega = 125.04 - 1934.136 * juliancentury
|
|
return e0 + 0.00256 * cos(radians(omega))
|
|
|
|
|
|
def sun_rt_ascension(juliancentury: float) -> float:
|
|
"""Calculate the sun's right ascension"""
|
|
oc = obliquity_correction(juliancentury)
|
|
al = sun_apparent_long(juliancentury)
|
|
|
|
tananum = cos(radians(oc)) * sin(radians(al))
|
|
tanadenom = cos(radians(al))
|
|
|
|
return degrees(atan2(tananum, tanadenom))
|
|
|
|
|
|
def sun_declination(juliancentury: float) -> float:
|
|
"""Calculate the sun's declination"""
|
|
e = obliquity_correction(juliancentury)
|
|
lambd = sun_apparent_long(juliancentury)
|
|
|
|
sint = sin(radians(e)) * sin(radians(lambd))
|
|
return degrees(asin(sint))
|
|
|
|
|
|
def var_y(juliancentury: float) -> float:
|
|
epsilon = obliquity_correction(juliancentury)
|
|
y = tan(radians(epsilon) / 2.0)
|
|
return y * y
|
|
|
|
|
|
def eq_of_time(juliancentury: float) -> Minutes:
|
|
l0 = geom_mean_long_sun(juliancentury)
|
|
e = eccentric_location_earth_orbit(juliancentury)
|
|
m = geom_mean_anomaly_sun(juliancentury)
|
|
|
|
y = var_y(juliancentury)
|
|
|
|
sin2l0 = sin(2.0 * radians(l0))
|
|
sinm = sin(radians(m))
|
|
cos2l0 = cos(2.0 * radians(l0))
|
|
sin4l0 = sin(4.0 * radians(l0))
|
|
sin2m = sin(2.0 * radians(m))
|
|
|
|
Etime = (
|
|
y * sin2l0
|
|
- 2.0 * e * sinm
|
|
+ 4.0 * e * y * sinm * cos2l0
|
|
- 0.5 * y * y * sin4l0
|
|
- 1.25 * e * e * sin2m
|
|
)
|
|
|
|
return degrees(Etime) * 4.0
|
|
|
|
|
|
def hour_angle(
|
|
latitude: float, declination: float, zenith: float, direction: SunDirection
|
|
) -> float:
|
|
"""Calculate the hour angle of the sun
|
|
|
|
See https://en.wikipedia.org/wiki/Hour_angle#Solar_hour_angle
|
|
|
|
Args:
|
|
latitude: The latitude of the obersver
|
|
declination: The declination of the sun
|
|
zenith: The zenith angle of the sun
|
|
direction: The direction of traversal of the sun
|
|
|
|
Raises:
|
|
ValueError
|
|
"""
|
|
|
|
latitude_rad = radians(latitude)
|
|
declination_rad = radians(declination)
|
|
zenith_rad = radians(zenith)
|
|
|
|
h = (cos(zenith_rad) - sin(latitude_rad) * sin(declination_rad)) / (
|
|
cos(latitude_rad) * cos(declination_rad)
|
|
)
|
|
|
|
hour_angle = acos(h)
|
|
if direction == SunDirection.SETTING:
|
|
hour_angle = -hour_angle
|
|
return hour_angle
|
|
|
|
|
|
def adjust_to_horizon(elevation: float) -> float:
|
|
"""Calculate the extra degrees of depression that you can see round the earth
|
|
due to the increase in elevation.
|
|
|
|
Args:
|
|
elevation: Elevation above the earth in metres
|
|
|
|
Returns:
|
|
A number of degrees to add to adjust for the elevation of the observer
|
|
"""
|
|
|
|
if elevation <= 0:
|
|
return 0
|
|
|
|
r = 6356900 # radius of the earth
|
|
a1 = r
|
|
h1 = r + elevation
|
|
theta1 = acos(a1 / h1)
|
|
return degrees(theta1)
|
|
|
|
|
|
def adjust_to_obscuring_feature(elevation: Tuple[float, float]) -> float:
|
|
"""Calculate the number of degrees to adjust for an obscuring feature"""
|
|
if elevation[0] == 0.0:
|
|
return 0.0
|
|
|
|
sign = -1 if elevation[0] < 0.0 else 1
|
|
return sign * degrees(
|
|
acos(fabs(elevation[0]) / sqrt(pow(elevation[0], 2) + pow(elevation[1], 2)))
|
|
)
|
|
|
|
|
|
def time_of_transit(
|
|
observer: Observer,
|
|
date: datetime.date,
|
|
zenith: float,
|
|
direction: SunDirection,
|
|
with_refraction: bool = True,
|
|
) -> datetime.datetime:
|
|
"""Calculate the time in the UTC timezone when the sun transits the
|
|
specificed zenith
|
|
|
|
Args:
|
|
observer: An observer viewing the sun at a specific, latitude, longitude
|
|
and elevation
|
|
date: The date to calculate for
|
|
zenith: The zenith angle for which to calculate the transit time
|
|
direction: The direction that the sun is traversing
|
|
|
|
Raises:
|
|
ValueError if the zenith is not transitted by the sun
|
|
|
|
Returns:
|
|
the time when the sun transits the specificed zenith
|
|
"""
|
|
if observer.latitude > 89.8:
|
|
latitude = 89.8
|
|
elif observer.latitude < -89.8:
|
|
latitude = -89.8
|
|
else:
|
|
latitude = observer.latitude
|
|
|
|
adjustment_for_elevation = 0.0
|
|
if isinstance(observer.elevation, float) and observer.elevation > 0.0:
|
|
adjustment_for_elevation = adjust_to_horizon(observer.elevation)
|
|
elif isinstance(observer.elevation, tuple):
|
|
adjustment_for_elevation = adjust_to_obscuring_feature(observer.elevation)
|
|
|
|
if with_refraction:
|
|
adjustment_for_refraction = refraction_at_zenith(
|
|
zenith + adjustment_for_elevation
|
|
)
|
|
else:
|
|
adjustment_for_refraction = 0.0
|
|
|
|
jd = julianday(date)
|
|
adjustment = 0.0
|
|
timeUTC = 0.0
|
|
|
|
for _ in range(2):
|
|
jc = julianday_to_juliancentury(jd + adjustment)
|
|
declination = sun_declination(jc)
|
|
|
|
hourangle = hour_angle(
|
|
latitude,
|
|
declination,
|
|
zenith + adjustment_for_elevation + adjustment_for_refraction,
|
|
direction,
|
|
)
|
|
|
|
delta = -observer.longitude - degrees(hourangle)
|
|
|
|
eqtime = eq_of_time(jc)
|
|
offset = delta * 4.0 - eqtime
|
|
|
|
if offset < -720.0:
|
|
offset += 1440
|
|
|
|
timeUTC = 720.0 + offset
|
|
adjustment = timeUTC / 1440.0
|
|
|
|
td = minutes_to_timedelta(timeUTC)
|
|
dt = datetime.datetime(date.year, date.month, date.day) + td
|
|
dt = dt.replace(tzinfo=datetime.timezone.utc) # pylint: disable=E1120
|
|
return dt
|
|
|
|
|
|
def time_at_elevation(
|
|
observer: Observer,
|
|
elevation: float,
|
|
date: Optional[datetime.date] = None,
|
|
direction: SunDirection = SunDirection.RISING,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
with_refraction: bool = True,
|
|
) -> datetime.datetime:
|
|
"""Calculates the time when the sun is at the specified elevation on the
|
|
specified date.
|
|
|
|
Note:
|
|
This method uses positive elevations for those above the horizon.
|
|
|
|
Elevations greater than 90 degrees are converted to a setting sun
|
|
i.e. an elevation of 110 will calculate a setting sun at 70 degrees.
|
|
|
|
Args:
|
|
elevation: Elevation of the sun in degrees above the horizon to calculate for.
|
|
observer: Observer to calculate for
|
|
date: Date to calculate for. Default is today's date in the timezone
|
|
`tzinfo`.
|
|
direction: Determines whether the calculated time is for the sun rising
|
|
or setting.
|
|
Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.
|
|
Default is rising.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which the sun is at the specified elevation.
|
|
"""
|
|
|
|
if elevation > 90.0:
|
|
elevation = 180.0 - elevation
|
|
direction = SunDirection.SETTING
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
zenith = 90 - elevation
|
|
try:
|
|
return time_of_transit(
|
|
observer, date, zenith, direction, with_refraction
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
except ValueError as exc:
|
|
if exc.args[0] == "math domain error":
|
|
raise ValueError(
|
|
f"Sun never reaches an elevation of {elevation} degrees "
|
|
"at this location."
|
|
) from exc
|
|
else:
|
|
raise
|
|
|
|
|
|
def noon(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> datetime.datetime:
|
|
"""Calculate solar noon time when the sun is at its highest point.
|
|
|
|
Args:
|
|
observer: An observer viewing the sun at a specific, latitude, longitude
|
|
and elevation
|
|
date: Date to calculate for. Default is today for the specified tzinfo.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which noon occurs.
|
|
"""
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
jc = julianday_to_juliancentury(julianday(date))
|
|
eqtime = eq_of_time(jc)
|
|
timeUTC = (720.0 - (4 * observer.longitude) - eqtime) / 60.0
|
|
|
|
hour = int(timeUTC)
|
|
minute = int((timeUTC - hour) * 60)
|
|
second = int((((timeUTC - hour) * 60) - minute) * 60)
|
|
|
|
if second > 59:
|
|
second -= 60
|
|
minute += 1
|
|
elif second < 0:
|
|
second += 60
|
|
minute -= 1
|
|
|
|
if minute > 59:
|
|
minute -= 60
|
|
hour += 1
|
|
elif minute < 0:
|
|
minute += 60
|
|
hour -= 1
|
|
|
|
if hour > 23:
|
|
hour -= 24
|
|
date += datetime.timedelta(days=1)
|
|
elif hour < 0:
|
|
hour += 24
|
|
date -= datetime.timedelta(days=1)
|
|
|
|
noon = datetime.datetime(
|
|
date.year,
|
|
date.month,
|
|
date.day,
|
|
hour,
|
|
minute,
|
|
second,
|
|
tzinfo=datetime.timezone.utc,
|
|
)
|
|
return noon.astimezone(tzinfo) # type: ignore pylint: disable=E1120
|
|
|
|
|
|
def midnight(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> datetime.datetime:
|
|
"""Calculate solar midnight time.
|
|
|
|
Note:
|
|
This calculates the solar midnight that is closest
|
|
to 00:00:00 of the specified date i.e. it may return a time that is on
|
|
the previous day.
|
|
|
|
Args:
|
|
observer: An observer viewing the sun at a specific, latitude, longitude
|
|
and elevation
|
|
date: Date to calculate for. Default is today for the specified tzinfo.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which midnight occurs.
|
|
"""
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
midday = datetime.time(12, 0, 0)
|
|
jd = julianday(datetime.datetime.combine(date, midday))
|
|
newt = julianday_to_juliancentury(jd + 0.5 + -observer.longitude / 360.0)
|
|
|
|
eqtime = eq_of_time(newt)
|
|
timeUTC = (-observer.longitude * 4.0) - eqtime
|
|
|
|
timeUTC = timeUTC / 60.0
|
|
hour = int(timeUTC)
|
|
minute = int((timeUTC - hour) * 60)
|
|
second = int((((timeUTC - hour) * 60) - minute) * 60)
|
|
|
|
if second > 59:
|
|
second -= 60
|
|
minute += 1
|
|
elif second < 0:
|
|
second += 60
|
|
minute -= 1
|
|
|
|
if minute > 59:
|
|
minute -= 60
|
|
hour += 1
|
|
elif minute < 0:
|
|
minute += 60
|
|
hour -= 1
|
|
|
|
if hour < 0:
|
|
hour += 24
|
|
date -= datetime.timedelta(days=1)
|
|
|
|
midnight = datetime.datetime(
|
|
date.year,
|
|
date.month,
|
|
date.day,
|
|
hour,
|
|
minute,
|
|
second,
|
|
tzinfo=datetime.timezone.utc,
|
|
)
|
|
return midnight.astimezone(tzinfo) # type: ignore
|
|
|
|
|
|
def zenith_and_azimuth(
|
|
observer: Observer,
|
|
dateandtime: datetime.datetime,
|
|
with_refraction: bool = True,
|
|
) -> Tuple[float, float]:
|
|
if observer.latitude > 89.8:
|
|
latitude = 89.8
|
|
elif observer.latitude < -89.8:
|
|
latitude = -89.8
|
|
else:
|
|
latitude = observer.latitude
|
|
|
|
longitude = observer.longitude
|
|
|
|
if dateandtime.tzinfo is None:
|
|
zone = 0.0
|
|
utc_datetime = dateandtime
|
|
else:
|
|
zone = -dateandtime.utcoffset().total_seconds() / 3600.0 # type: ignore
|
|
utc_datetime = dateandtime.astimezone(datetime.timezone.utc)
|
|
|
|
jd = julianday(utc_datetime)
|
|
t = julianday_to_juliancentury(jd)
|
|
declination = sun_declination(t)
|
|
eqtime = eq_of_time(t)
|
|
|
|
# 360deg * 4 == 1440 minutes, 60*24 = 1440 minutes == 1 rotation
|
|
solarTimeFix = eqtime + (4.0 * longitude) + (60 * zone)
|
|
trueSolarTime = (
|
|
dateandtime.hour * 60.0
|
|
+ dateandtime.minute
|
|
+ dateandtime.second / 60.0
|
|
+ solarTimeFix
|
|
)
|
|
# in minutes as a float, fractional part is seconds
|
|
|
|
while trueSolarTime > 1440:
|
|
trueSolarTime = trueSolarTime - 1440
|
|
|
|
hourangle = trueSolarTime / 4.0 - 180.0
|
|
# Thanks to Louis Schwarzmayr for the next line:
|
|
if hourangle < -180:
|
|
hourangle = hourangle + 360.0
|
|
|
|
ch = cos(radians(hourangle))
|
|
# sh = sin(radians(hourangle))
|
|
cl = cos(radians(latitude))
|
|
sl = sin(radians(latitude))
|
|
sd = sin(radians(declination))
|
|
cd = cos(radians(declination))
|
|
|
|
csz = cl * cd * ch + sl * sd
|
|
|
|
if csz > 1.0:
|
|
csz = 1.0
|
|
elif csz < -1.0:
|
|
csz = -1.0
|
|
|
|
zenith = degrees(acos(csz))
|
|
|
|
azDenom = cl * sin(radians(zenith))
|
|
|
|
if abs(azDenom) > 0.001:
|
|
azRad = ((sl * cos(radians(zenith))) - sd) / azDenom
|
|
|
|
if abs(azRad) > 1.0:
|
|
if azRad < 0:
|
|
azRad = -1.0
|
|
else:
|
|
azRad = 1.0
|
|
|
|
azimuth = 180.0 - degrees(acos(azRad))
|
|
|
|
if hourangle > 0.0:
|
|
azimuth = -azimuth
|
|
else:
|
|
if latitude > 0.0:
|
|
azimuth = 180.0
|
|
else:
|
|
azimuth = 0.0
|
|
|
|
if azimuth < 0.0:
|
|
azimuth = azimuth + 360.0
|
|
|
|
if with_refraction:
|
|
zenith -= refraction_at_zenith(zenith)
|
|
# elevation = 90 - zenith
|
|
|
|
return zenith, azimuth
|
|
|
|
|
|
def zenith(
|
|
observer: Observer,
|
|
dateandtime: Optional[datetime.datetime] = None,
|
|
with_refraction: bool = True,
|
|
) -> float:
|
|
"""Calculate the zenith angle of the sun.
|
|
|
|
Args:
|
|
observer: Observer to calculate the solar zenith for
|
|
dateandtime: The date and time for which to calculate the angle.
|
|
If `dateandtime` is None or is a naive Python datetime
|
|
then it is assumed to be in the UTC timezone.
|
|
with_refraction: If True adjust zenith to take refraction into account
|
|
|
|
Returns:
|
|
The zenith angle in degrees.
|
|
"""
|
|
|
|
if dateandtime is None:
|
|
dateandtime = now(datetime.timezone.utc)
|
|
|
|
return zenith_and_azimuth(observer, dateandtime, with_refraction)[0]
|
|
|
|
|
|
def azimuth(
|
|
observer: Observer,
|
|
dateandtime: Optional[datetime.datetime] = None,
|
|
) -> float:
|
|
"""Calculate the azimuth angle of the sun.
|
|
|
|
Args:
|
|
observer: Observer to calculate the solar azimuth for
|
|
dateandtime: The date and time for which to calculate the angle.
|
|
If `dateandtime` is None or is a naive Python datetime
|
|
then it is assumed to be in the UTC timezone.
|
|
|
|
Returns:
|
|
The azimuth angle in degrees clockwise from North.
|
|
|
|
If `dateandtime` is a naive Python datetime then it is assumed to be
|
|
in the UTC timezone.
|
|
"""
|
|
|
|
if dateandtime is None:
|
|
dateandtime = now(datetime.timezone.utc)
|
|
|
|
return zenith_and_azimuth(observer, dateandtime)[1]
|
|
|
|
|
|
def elevation(
|
|
observer: Observer,
|
|
dateandtime: Optional[datetime.datetime] = None,
|
|
with_refraction: bool = True,
|
|
) -> float:
|
|
"""Calculate the sun's angle of elevation.
|
|
|
|
Args:
|
|
observer: Observer to calculate the solar elevation for
|
|
dateandtime: The date and time for which to calculate the angle.
|
|
If `dateandtime` is None or is a naive Python datetime
|
|
then it is assumed to be in the UTC timezone.
|
|
with_refraction: If True adjust elevation to take refraction into account
|
|
|
|
Returns:
|
|
The elevation angle in degrees above the horizon.
|
|
"""
|
|
|
|
if dateandtime is None:
|
|
dateandtime = now(datetime.timezone.utc)
|
|
|
|
return 90.0 - zenith(observer, dateandtime, with_refraction)
|
|
|
|
|
|
def dawn(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
depression: Union[float, Depression] = Depression.CIVIL,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> datetime.datetime:
|
|
"""Calculate dawn time.
|
|
|
|
Args:
|
|
observer: Observer to calculate dawn for
|
|
date: Date to calculate for. Default is today's date in the
|
|
timezone `tzinfo`.
|
|
depression: Number of degrees below the horizon to use to calculate dawn.
|
|
Default is for Civil dawn i.e. 6.0
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which dawn occurs.
|
|
|
|
Raises:
|
|
ValueError: if dawn does not occur on the specified date
|
|
"""
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
elif isinstance(date, datetime.datetime):
|
|
tzinfo = date.tzinfo or tzinfo
|
|
date = date.date()
|
|
|
|
dep: float = 0.0
|
|
if isinstance(depression, Depression):
|
|
dep = depression.value
|
|
else:
|
|
dep = depression
|
|
|
|
try:
|
|
tot = time_of_transit(
|
|
observer, date, 90.0 + dep, SunDirection.RISING
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
|
|
# If the dates don't match search on either the next or previous day.
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
if tot_date < date:
|
|
delta = datetime.timedelta(days=1)
|
|
else:
|
|
delta = datetime.timedelta(days=-1)
|
|
new_date = date + delta
|
|
|
|
tot = time_of_transit(
|
|
observer,
|
|
new_date,
|
|
90.0 + dep,
|
|
SunDirection.RISING,
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
# Still can't get a time then raise the error
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
raise ValueError("Unable to find a dawn time on the date specified")
|
|
return tot
|
|
except ValueError as exc:
|
|
if exc.args[0] == "math domain error":
|
|
raise ValueError(
|
|
f"Sun never reaches {dep} degrees below the horizon, at this location."
|
|
) from exc
|
|
else:
|
|
raise
|
|
|
|
|
|
def sunrise(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> datetime.datetime:
|
|
"""Calculate sunrise time.
|
|
|
|
Args:
|
|
observer: Observer to calculate sunrise for
|
|
date: Date to calculate for. Default is today's date in the
|
|
timezone `tzinfo`.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which sunrise occurs.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not reach the horizon on the specified date
|
|
"""
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
elif isinstance(date, datetime.datetime):
|
|
tzinfo = date.tzinfo or tzinfo
|
|
date = date.date()
|
|
|
|
try:
|
|
tot = time_of_transit(
|
|
observer,
|
|
date,
|
|
90.0 + SUN_APPARENT_RADIUS,
|
|
SunDirection.RISING,
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
if tot_date < date:
|
|
delta = datetime.timedelta(days=1)
|
|
else:
|
|
delta = datetime.timedelta(days=-1)
|
|
new_date = date + delta
|
|
|
|
tot = time_of_transit(
|
|
observer,
|
|
new_date,
|
|
90.0 + SUN_APPARENT_RADIUS,
|
|
SunDirection.RISING,
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
raise ValueError("Unable to find a sunrise time on the date specified")
|
|
return tot
|
|
except ValueError as exc:
|
|
if exc.args[0] == "math domain error":
|
|
z = zenith(observer, noon(observer, date))
|
|
if z > 90.0:
|
|
msg = "Sun is always below the horizon on this day, at this location."
|
|
else:
|
|
msg = "Sun is always above the horizon on this day, at this location."
|
|
raise ValueError(msg) from exc
|
|
else:
|
|
raise
|
|
|
|
|
|
def sunset(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> datetime.datetime:
|
|
"""Calculate sunset time.
|
|
|
|
Args:
|
|
observer: Observer to calculate sunset for
|
|
date: Date to calculate for. Default is today's date in the
|
|
timezone `tzinfo`.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which sunset occurs.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not reach the horizon
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
elif isinstance(date, datetime.datetime):
|
|
tzinfo = date.tzinfo or tzinfo
|
|
date = date.date()
|
|
|
|
try:
|
|
tot = time_of_transit(
|
|
observer,
|
|
date,
|
|
90.0 + SUN_APPARENT_RADIUS,
|
|
SunDirection.SETTING,
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
if tot_date < date:
|
|
delta = datetime.timedelta(days=1)
|
|
else:
|
|
delta = datetime.timedelta(days=-1)
|
|
new_date = date + delta
|
|
|
|
tot = time_of_transit(
|
|
observer,
|
|
new_date,
|
|
90.0 + SUN_APPARENT_RADIUS,
|
|
SunDirection.SETTING,
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
raise ValueError("Unable to find a sunset time on the date specified")
|
|
return tot
|
|
except ValueError as exc:
|
|
if exc.args[0] == "math domain error":
|
|
z = zenith(observer, noon(observer, date))
|
|
if z > 90.0:
|
|
msg = "Sun is always below the horizon on this day, at this location."
|
|
else:
|
|
msg = "Sun is always above the horizon on this day, at this location."
|
|
raise ValueError(msg) from exc
|
|
else:
|
|
raise
|
|
|
|
|
|
def dusk(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
depression: Union[float, Depression] = Depression.CIVIL,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> datetime.datetime:
|
|
"""Calculate dusk time.
|
|
|
|
Args:
|
|
observer: Observer to calculate dusk for
|
|
date: Date to calculate for. Default is today's date in the
|
|
timezone `tzinfo`.
|
|
depression: Number of degrees below the horizon to use to calculate dusk.
|
|
Default is for Civil dusk i.e. 6.0
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Date and time at which dusk occurs.
|
|
|
|
Raises:
|
|
ValueError: if dusk does not occur on the specified date
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
elif isinstance(date, datetime.datetime):
|
|
tzinfo = date.tzinfo or tzinfo
|
|
date = date.date()
|
|
|
|
dep: float = 0.0
|
|
if isinstance(depression, Depression):
|
|
dep = depression.value
|
|
else:
|
|
dep = depression
|
|
|
|
try:
|
|
tot = time_of_transit(
|
|
observer, date, 90.0 + dep, SunDirection.SETTING
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
if tot_date < date:
|
|
delta = datetime.timedelta(days=1)
|
|
else:
|
|
delta = datetime.timedelta(days=-1)
|
|
new_date = date + delta
|
|
|
|
tot = time_of_transit(
|
|
observer,
|
|
new_date,
|
|
90.0 + dep,
|
|
SunDirection.SETTING,
|
|
).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
tot_date = tot.date()
|
|
if tot_date != date:
|
|
raise ValueError("Unable to find a dusk time on the date specified")
|
|
return tot
|
|
except ValueError as exc:
|
|
if exc.args[0] == "math domain error":
|
|
raise ValueError(
|
|
f"Sun never reaches {dep} degrees below the horizon, at this location."
|
|
) from exc
|
|
else:
|
|
raise
|
|
|
|
|
|
def daylight(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> TimePeriod:
|
|
"""Calculate daylight start and end times.
|
|
|
|
Args:
|
|
observer: Observer to calculate daylight for
|
|
date: Date to calculate for. Default is today's date in the
|
|
timezone `tzinfo`.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
A tuple of the date and time at which daylight starts and ends.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not rise or does not set
|
|
"""
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
sr = sunrise(observer, date, tzinfo)
|
|
ss = sunset(observer, date, tzinfo)
|
|
|
|
return sr, ss
|
|
|
|
|
|
def night(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> TimePeriod:
|
|
"""Calculate night start and end times.
|
|
|
|
Night is calculated to be between astronomical dusk on the
|
|
date specified and astronomical dawn of the next day.
|
|
|
|
Args:
|
|
observer: Observer to calculate night for
|
|
date: Date to calculate for. Default is today's date for the
|
|
specified tzinfo.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
A tuple of the date and time at which night starts and ends.
|
|
|
|
Raises:
|
|
ValueError: if dawn does not occur on the specified date or
|
|
dusk on the following day
|
|
"""
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
start = dusk(observer, date, 6, tzinfo)
|
|
tomorrow = date + datetime.timedelta(days=1)
|
|
end = dawn(observer, tomorrow, 6, tzinfo)
|
|
|
|
return start, end
|
|
|
|
|
|
def twilight(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
direction: SunDirection = SunDirection.RISING,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> TimePeriod:
|
|
"""Returns the start and end times of Twilight
|
|
when the sun is traversing in the specified direction.
|
|
|
|
This method defines twilight as being between the time
|
|
when the sun is at -6 degrees and sunrise/sunset.
|
|
|
|
Args:
|
|
observer: Observer to calculate twilight for
|
|
date: Date for which to calculate the times.
|
|
Default is today's date in the timezone `tzinfo`.
|
|
direction: Determines whether the time is for the sun rising or setting.
|
|
Use ``astral.SunDirection.RISING`` or
|
|
``astral.SunDirection.SETTING``.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
A tuple of the date and time at which twilight starts and ends.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not rise or does not set
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
start = time_of_transit(observer, date, 90 + 6, direction,).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
if direction == SunDirection.RISING:
|
|
end = sunrise(observer, date, tzinfo).astimezone(tzinfo) # type: ignore
|
|
else:
|
|
end = sunset(observer, date, tzinfo).astimezone(tzinfo) # type: ignore
|
|
|
|
if direction == SunDirection.RISING:
|
|
return start, end
|
|
else:
|
|
return end, start
|
|
|
|
|
|
def golden_hour(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
direction: SunDirection = SunDirection.RISING,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> TimePeriod:
|
|
"""Returns the start and end times of the Golden Hour
|
|
when the sun is traversing in the specified direction.
|
|
|
|
This method uses the definition from PhotoPills i.e. the
|
|
golden hour is when the sun is between 4 degrees below the horizon
|
|
and 6 degrees above.
|
|
|
|
Args:
|
|
observer: Observer to calculate the golden hour for
|
|
date: Date for which to calculate the times.
|
|
Default is today's date in the timezone `tzinfo`.
|
|
direction: Determines whether the time is for the sun rising or setting.
|
|
Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
A tuple of the date and time at which the Golden Hour starts and ends.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not transit the elevations -4 & +6 degrees
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
start = time_of_transit(observer, date, 90 + 4, direction,).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
end = time_of_transit(observer, date, 90 - 6, direction,).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
|
|
if direction == SunDirection.RISING:
|
|
return start, end
|
|
else:
|
|
return end, start
|
|
|
|
|
|
def blue_hour(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
direction: SunDirection = SunDirection.RISING,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> TimePeriod:
|
|
"""Returns the start and end times of the Blue Hour
|
|
when the sun is traversing in the specified direction.
|
|
|
|
This method uses the definition from PhotoPills i.e. the
|
|
blue hour is when the sun is between 6 and 4 degrees below the horizon.
|
|
|
|
Args:
|
|
observer: Observer to calculate the blue hour for
|
|
date: Date for which to calculate the times.
|
|
Default is today's date in the timezone `tzinfo`.
|
|
direction: Determines whether the time is for the sun rising or setting.
|
|
Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
A tuple of the date and time at which the Blue Hour starts and ends.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not transit the elevations -4 & -6 degrees
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
start = time_of_transit(observer, date, 90 + 6, direction,).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
end = time_of_transit(observer, date, 90 + 4, direction,).astimezone(
|
|
tzinfo # type: ignore
|
|
)
|
|
|
|
if direction == SunDirection.RISING:
|
|
return start, end
|
|
else:
|
|
return end, start
|
|
|
|
|
|
def rahukaalam(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
daytime: bool = True,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> TimePeriod:
|
|
"""Calculate ruhakaalam times.
|
|
|
|
Args:
|
|
observer: Observer to calculate rahukaalam for
|
|
date: Date to calculate for. Default is today's date in the
|
|
timezone `tzinfo`.
|
|
daytime: If True calculate for the day time else calculate for the
|
|
night time.
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Tuple containing the start and end times for Rahukaalam.
|
|
|
|
Raises:
|
|
ValueError: if the sun does not rise or does not set
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
if daytime:
|
|
start = sunrise(observer, date, tzinfo)
|
|
end = sunset(observer, date, tzinfo)
|
|
else:
|
|
start = sunset(observer, date, tzinfo)
|
|
oneday = datetime.timedelta(days=1)
|
|
end = sunrise(observer, date + oneday, tzinfo)
|
|
|
|
octant_duration = datetime.timedelta(seconds=(end - start).seconds / 8)
|
|
|
|
# Mo,Sa,Fr,We,Th,Tu,Su
|
|
octant_index = [1, 6, 4, 5, 3, 2, 7]
|
|
|
|
weekday = date.weekday()
|
|
octant = octant_index[weekday]
|
|
|
|
start = start + (octant_duration * octant)
|
|
end = start + octant_duration
|
|
|
|
return start, end
|
|
|
|
|
|
def sun(
|
|
observer: Observer,
|
|
date: Optional[datetime.date] = None,
|
|
dawn_dusk_depression: Union[float, Depression] = Depression.CIVIL,
|
|
tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,
|
|
) -> Dict[str, datetime.datetime]:
|
|
"""Calculate all the info for the sun at once.
|
|
|
|
Args:
|
|
observer: Observer for which to calculate the times of the sun
|
|
date: Date to calculate for.
|
|
Default is today's date in the timezone `tzinfo`.
|
|
dawn_dusk_depression: Depression to use to calculate dawn and dusk.
|
|
Default is for Civil dusk i.e. 6.0
|
|
tzinfo: Timezone to return times in. Default is UTC.
|
|
|
|
Returns:
|
|
Dictionary with keys ``dawn``, ``sunrise``, ``noon``, ``sunset`` and ``dusk``
|
|
whose values are the results of the corresponding functions.
|
|
|
|
Raises:
|
|
ValueError: if passed through from any of the functions
|
|
"""
|
|
|
|
if isinstance(tzinfo, str):
|
|
tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore
|
|
|
|
if date is None:
|
|
date = today(tzinfo) # type: ignore
|
|
|
|
return {
|
|
"dawn": dawn(observer, date, dawn_dusk_depression, tzinfo),
|
|
"sunrise": sunrise(observer, date, tzinfo),
|
|
"noon": noon(observer, date, tzinfo),
|
|
"sunset": sunset(observer, date, tzinfo),
|
|
"dusk": dusk(observer, date, dawn_dusk_depression, tzinfo),
|
|
}
|