1import datetime
2from math import (
3    acos,
4    asin,
5    atan2,
6    cos,
7    degrees,
8    fabs,
9    floor,
10    radians,
11    sin,
12    sqrt,
13    tan,
14)
15from typing import Dict, Optional, Tuple, Union
16
17import pytz
18
19from astral import Depression, Observer, SunDirection, now, today
20
21__all__ = [
22    "sun",
23    "dawn",
24    "sunrise",
25    "noon",
26    "midnight",
27    "sunset",
28    "dusk",
29    "daylight",
30    "night",
31    "twilight",
32    "blue_hour",
33    "golden_hour",
34    "rahukaalam",
35    "zenith",
36    "azimuth",
37    "elevation",
38    "time_at_elevation",
39]
40
41
42# Using 32 arc minutes as sun's apparent diameter
43SUN_APPARENT_RADIUS = 32.0 / (60.0 * 2.0)
44
45
46def julianday(date: datetime.date) -> float:
47    """Calculate the Julian Day for the specified date"""
48    y = date.year
49    m = date.month
50    d = date.day
51
52    if m <= 2:
53        y -= 1
54        m += 12
55
56    a = floor(y / 100)
57    b = 2 - a + floor(a / 4)
58    jd = floor(365.25 * (y + 4716)) + floor(30.6001 * (m + 1)) + d + b - 1524.5
59
60    return jd
61
62
63def minutes_to_timedelta(minutes: float) -> datetime.timedelta:
64    """Convert a floating point number of minutes to a :class:`~datetime.timedelta`"""
65    d = int(minutes / 1440)
66    minutes = minutes - (d * 1440)
67    minutes = minutes * 60
68    s = int(minutes)
69    sfrac = minutes - s
70    us = int(sfrac * 1_000_000)
71
72    return datetime.timedelta(days=d, seconds=s, microseconds=us)
73
74
75def jday_to_jcentury(julianday: float) -> float:
76    """Convert a Julian Day number to a Julian Century"""
77    return (julianday - 2451545.0) / 36525.0
78
79
80def jcentury_to_jday(juliancentury: float) -> float:
81    """Convert a Julian Century number to a Julian Day"""
82    return (juliancentury * 36525.0) + 2451545.0
83
84
85def geom_mean_long_sun(juliancentury: float) -> float:
86    """Calculate the geometric mean longitude of the sun"""
87    l0 = 280.46646 + juliancentury * (36000.76983 + 0.0003032 * juliancentury)
88    return l0 % 360.0
89
90
91def geom_mean_anomaly_sun(juliancentury: float) -> float:
92    """Calculate the geometric mean anomaly of the sun"""
93    return 357.52911 + juliancentury * (35999.05029 - 0.0001537 * juliancentury)
94
95
96def eccentric_location_earth_orbit(juliancentury: float) -> float:
97    """Calculate the eccentricity of Earth's orbit"""
98    return 0.016708634 - juliancentury * (0.000042037 + 0.0000001267 * juliancentury)
99
100
101def sun_eq_of_center(juliancentury: float) -> float:
102    """Calculate the equation of the center of the sun"""
103    m = geom_mean_anomaly_sun(juliancentury)
104
105    mrad = radians(m)
106    sinm = sin(mrad)
107    sin2m = sin(mrad + mrad)
108    sin3m = sin(mrad + mrad + mrad)
109
110    c = (
111        sinm * (1.914602 - juliancentury * (0.004817 + 0.000014 * juliancentury))
112        + sin2m * (0.019993 - 0.000101 * juliancentury)
113        + sin3m * 0.000289
114    )
115
116    return c
117
118
119def sun_true_long(juliancentury: float) -> float:
120    """Calculate the sun's true longitude"""
121    l0 = geom_mean_long_sun(juliancentury)
122    c = sun_eq_of_center(juliancentury)
123
124    return l0 + c
125
126
127def sun_true_anomoly(juliancentury: float) -> float:
128    """Calculate the sun's true anomaly"""
129    m = geom_mean_anomaly_sun(juliancentury)
130    c = sun_eq_of_center(juliancentury)
131
132    return m + c
133
134
135def sun_rad_vector(juliancentury: float) -> float:
136    v = sun_true_anomoly(juliancentury)
137    e = eccentric_location_earth_orbit(juliancentury)
138
139    return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v)))
140
141
142def sun_apparent_long(juliancentury: float) -> float:
143    true_long = sun_true_long(juliancentury)
144
145    omega = 125.04 - 1934.136 * juliancentury
146    return true_long - 0.00569 - 0.00478 * sin(radians(omega))
147
148
149def mean_obliquity_of_ecliptic(juliancentury: float) -> float:
150    seconds = 21.448 - juliancentury * (
151        46.815 + juliancentury * (0.00059 - juliancentury * (0.001813))
152    )
153    return 23.0 + (26.0 + (seconds / 60.0)) / 60.0
154
155
156def obliquity_correction(juliancentury: float) -> float:
157    e0 = mean_obliquity_of_ecliptic(juliancentury)
158
159    omega = 125.04 - 1934.136 * juliancentury
160    return e0 + 0.00256 * cos(radians(omega))
161
162
163def sun_rt_ascension(juliancentury: float) -> float:
164    """Calculate the sun's right ascension"""
165    oc = obliquity_correction(juliancentury)
166    al = sun_apparent_long(juliancentury)
167
168    tananum = cos(radians(oc)) * sin(radians(al))
169    tanadenom = cos(radians(al))
170
171    return degrees(atan2(tananum, tanadenom))
172
173
174def sun_declination(juliancentury: float) -> float:
175    """Calculate the sun's declination"""
176    e = obliquity_correction(juliancentury)
177    lambd = sun_apparent_long(juliancentury)
178
179    sint = sin(radians(e)) * sin(radians(lambd))
180    return degrees(asin(sint))
181
182
183def var_y(juliancentury: float) -> float:
184    epsilon = obliquity_correction(juliancentury)
185    y = tan(radians(epsilon) / 2.0)
186    return y * y
187
188
189def eq_of_time(juliancentury: float) -> float:
190    l0 = geom_mean_long_sun(juliancentury)
191    e = eccentric_location_earth_orbit(juliancentury)
192    m = geom_mean_anomaly_sun(juliancentury)
193
194    y = var_y(juliancentury)
195
196    sin2l0 = sin(2.0 * radians(l0))
197    sinm = sin(radians(m))
198    cos2l0 = cos(2.0 * radians(l0))
199    sin4l0 = sin(4.0 * radians(l0))
200    sin2m = sin(2.0 * radians(m))
201
202    Etime = (
203        y * sin2l0
204        - 2.0 * e * sinm
205        + 4.0 * e * y * sinm * cos2l0
206        - 0.5 * y * y * sin4l0
207        - 1.25 * e * e * sin2m
208    )
209
210    return degrees(Etime) * 4.0
211
212
213def hour_angle(
214    latitude: float, declination: float, zenith: float, direction: SunDirection
215) -> float:
216    """Calculate the hour angle of the sun
217
218    See https://en.wikipedia.org/wiki/Hour_angle#Solar_hour_angle
219
220    Args:
221        latitude: The latitude of the obersver
222        declination: The declination of the sun
223        zenith: The zenith angle of the sun
224        direction: The direction of traversal of the sun
225
226    Raises:
227        ValueError
228    """
229
230    latitude_rad = radians(latitude)
231    declination_rad = radians(declination)
232    zenith_rad = radians(zenith)
233
234    # n = cos(zenith_rad)
235    # d = cos(latitude_rad) * cos(declination_rad)
236    # t = tan(latitude_rad) * tan(declination_rad)
237    # h = (n / d) - t
238
239    h = (cos(zenith_rad) - sin(latitude_rad) * sin(declination_rad)) / (
240        cos(latitude_rad) * cos(declination_rad)
241    )
242
243    HA = acos(h)
244    if direction == SunDirection.SETTING:
245        HA = -HA
246    return HA
247
248
249def adjust_to_horizon(elevation: float) -> float:
250    """Calculate the extra degrees of depression that you can see round the earth
251    due to the increase in elevation.
252
253    Args:
254        elevation: Elevation above the earth in metres
255
256    Returns:
257        A number of degrees to add to adjust for the elevation of the observer
258    """
259
260    if elevation <= 0:
261        return 0
262
263    r = 6356900  # radius of the earth
264    a1 = r
265    h1 = r + elevation
266    theta1 = acos(a1 / h1)
267    return degrees(theta1)
268
269
270def adjust_to_obscuring_feature(elevation: Tuple[float, float]) -> float:
271    """Calculate the number of degrees to adjust for an obscuring feature"""
272    if elevation[0] == 0.0:
273        return 0.0
274
275    sign = -1 if elevation[0] < 0.0 else 1
276    return sign * degrees(
277        acos(fabs(elevation[0]) / sqrt(pow(elevation[0], 2) + pow(elevation[1], 2)))
278    )
279
280
281def refraction_at_zenith(zenith: float) -> float:
282    """Calculate the degrees of refraction of the sun due to the sun's elevation."""
283
284    elevation = 90 - zenith
285    if elevation >= 85.0:
286        return 0
287
288    refractionCorrection = 0.0
289    te = tan(radians(elevation))
290    if elevation > 5.0:
291        refractionCorrection = (
292            58.1 / te - 0.07 / (te * te * te) + 0.000086 / (te * te * te * te * te)
293        )
294    elif elevation > -0.575:
295        step1 = -12.79 + elevation * 0.711
296        step2 = 103.4 + elevation * step1
297        step3 = -518.2 + elevation * step2
298        refractionCorrection = 1735.0 + elevation * step3
299    else:
300        refractionCorrection = -20.774 / te
301
302    refractionCorrection = refractionCorrection / 3600.0
303
304    return refractionCorrection
305
306
307def time_of_transit(
308    observer: Observer, date: datetime.date, zenith: float, direction: SunDirection
309) -> datetime.datetime:
310    """Calculate the time in the UTC timezone when the sun transits the specificed zenith
311
312    Args:
313        observer: An observer viewing the sun at a specific, latitude, longitude and elevation
314        date: The date to calculate for
315        zenith: The zenith angle for which to calculate the transit time
316        direction: The direction that the sun is traversing
317
318    Raises:
319        ValueError if the zenith is not transitted by the sun
320
321    Returns:
322        the time when the sun transits the specificed zenith
323    """
324    if observer.latitude > 89.8:
325        latitude = 89.8
326    elif observer.latitude < -89.8:
327        latitude = -89.8
328    else:
329        latitude = observer.latitude
330
331    adjustment_for_elevation = 0.0
332    if isinstance(observer.elevation, float) and observer.elevation > 0.0:
333        adjustment_for_elevation = adjust_to_horizon(observer.elevation)
334    elif isinstance(observer.elevation, tuple):
335        adjustment_for_elevation = adjust_to_obscuring_feature(observer.elevation)
336
337    adjustment_for_refraction = refraction_at_zenith(zenith + adjustment_for_elevation)
338
339    jd = julianday(date)
340    t = jday_to_jcentury(jd)
341    solarDec = sun_declination(t)
342
343    hourangle = hour_angle(
344        latitude,
345        solarDec,
346        zenith + adjustment_for_elevation - adjustment_for_refraction,
347        direction,
348    )
349
350    delta = -observer.longitude - degrees(hourangle)
351    timeDiff = 4.0 * delta
352    timeUTC = 720.0 + timeDiff - eq_of_time(t)
353
354    t = jday_to_jcentury(jcentury_to_jday(t) + timeUTC / 1440.0)
355    solarDec = sun_declination(t)
356    hourangle = hour_angle(
357        latitude,
358        solarDec,
359        zenith + adjustment_for_elevation + adjustment_for_refraction,
360        direction,
361    )
362
363    delta = -observer.longitude - degrees(hourangle)
364    timeDiff = 4.0 * delta
365    timeUTC = 720 + timeDiff - eq_of_time(t)
366
367    td = minutes_to_timedelta(timeUTC)
368    dt = datetime.datetime(date.year, date.month, date.day) + td
369    dt = pytz.utc.localize(dt)  # pylint: disable=E1120
370    return dt
371
372
373def time_at_elevation(
374    observer: Observer,
375    elevation: float,
376    date: Optional[datetime.date] = None,
377    direction: SunDirection = SunDirection.RISING,
378    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
379) -> datetime.datetime:
380    """Calculates the time when the sun is at the specified elevation on the specified date.
381
382    Note:
383        This method uses positive elevations for those above the horizon.
384
385        Elevations greater than 90 degrees are converted to a setting sun
386        i.e. an elevation of 110 will calculate a setting sun at 70 degrees.
387
388    Args:
389        elevation: Elevation of the sun in degrees above the horizon to calculate for.
390        observer:  Observer to calculate for
391        date:      Date to calculate for. Default is today's date in the timezone `tzinfo`.
392        direction: Determines whether the calculated time is for the sun rising or setting.
393                   Use ``SunDirection.RISING`` or ``SunDirection.SETTING``. Default is rising.
394        tzinfo:    Timezone to return times in. Default is UTC.
395
396    Returns:
397        Date and time at which the sun is at the specified elevation.
398    """
399
400    if elevation > 90.0:
401        elevation = 180.0 - elevation
402        direction = SunDirection.SETTING
403
404    if isinstance(tzinfo, str):
405        tzinfo = pytz.timezone(tzinfo)
406
407    if date is None:
408        date = today(tzinfo)
409
410    zenith = 90 - elevation
411    try:
412        return time_of_transit(observer, date, zenith, direction).astimezone(tzinfo)
413    except ValueError as exc:
414        if exc.args[0] == "math domain error":
415            raise ValueError(
416                f"Sun never reaches an elevation of {elevation} degrees "
417                "at this location."
418            ) from exc
419        else:
420            raise
421
422
423def noon(
424    observer: Observer,
425    date: Optional[datetime.date] = None,
426    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
427) -> datetime.datetime:
428    """Calculate solar noon time when the sun is at its highest point.
429
430    Args:
431        observer: An observer viewing the sun at a specific, latitude, longitude and elevation
432        date:     Date to calculate for. Default is today for the specified tzinfo.
433        tzinfo:   Timezone to return times in. Default is UTC.
434
435    Returns:
436        Date and time at which noon occurs.
437    """
438    if isinstance(tzinfo, str):
439        tzinfo = pytz.timezone(tzinfo)
440
441    if date is None:
442        date = today(tzinfo)
443
444    jc = jday_to_jcentury(julianday(date))
445    eqtime = eq_of_time(jc)
446    timeUTC = (720.0 - (4 * observer.longitude) - eqtime) / 60.0
447
448    hour = int(timeUTC)
449    minute = int((timeUTC - hour) * 60)
450    second = int((((timeUTC - hour) * 60) - minute) * 60)
451
452    if second > 59:
453        second -= 60
454        minute += 1
455    elif second < 0:
456        second += 60
457        minute -= 1
458
459    if minute > 59:
460        minute -= 60
461        hour += 1
462    elif minute < 0:
463        minute += 60
464        hour -= 1
465
466    if hour > 23:
467        hour -= 24
468        date += datetime.timedelta(days=1)
469    elif hour < 0:
470        hour += 24
471        date -= datetime.timedelta(days=1)
472
473    noon = datetime.datetime(date.year, date.month, date.day, hour, minute, second)
474    return pytz.utc.localize(noon).astimezone(tzinfo)  # pylint: disable=E1120
475
476
477def midnight(
478    observer: Observer,
479    date: Optional[datetime.date] = None,
480    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
481) -> datetime.datetime:
482    """Calculate solar midnight time.
483
484    Note:
485        This calculates the solar midnight that is closest
486        to 00:00:00 of the specified date i.e. it may return a time that is on
487        the previous day.
488
489    Args:
490        observer: An observer viewing the sun at a specific, latitude, longitude and elevation
491        date:     Date to calculate for. Default is today for the specified tzinfo.
492        tzinfo:   Timezone to return times in. Default is UTC.
493
494    Returns:
495        Date and time at which midnight occurs.
496    """
497    if isinstance(tzinfo, str):
498        tzinfo = pytz.timezone(tzinfo)
499
500    if date is None:
501        date = today(tzinfo)
502
503    jd = julianday(date)
504    newt = jday_to_jcentury(jd + 0.5 + -observer.longitude / 360.0)
505
506    eqtime = eq_of_time(newt)
507    timeUTC = (-observer.longitude * 4.0) - eqtime
508
509    timeUTC = timeUTC / 60.0
510    hour = int(timeUTC)
511    minute = int((timeUTC - hour) * 60)
512    second = int((((timeUTC - hour) * 60) - minute) * 60)
513
514    if second > 59:
515        second -= 60
516        minute += 1
517    elif second < 0:
518        second += 60
519        minute -= 1
520
521    if minute > 59:
522        minute -= 60
523        hour += 1
524    elif minute < 0:
525        minute += 60
526        hour -= 1
527
528    if hour < 0:
529        hour += 24
530        date -= datetime.timedelta(days=1)
531
532    midnight = datetime.datetime(date.year, date.month, date.day, hour, minute, second)
533    return pytz.utc.localize(midnight).astimezone(tzinfo)  # pylint: disable=E1120
534
535
536def zenith_and_azimuth(
537    observer: Observer, dateandtime: datetime.datetime, with_refraction: bool = True,
538) -> Tuple[float, float]:
539    if observer.latitude > 89.8:
540        latitude = 89.8
541    elif observer.latitude < -89.8:
542        latitude = -89.8
543    else:
544        latitude = observer.latitude
545
546    longitude = observer.longitude
547
548    if dateandtime.tzinfo is None:
549        zone = 0.0
550        utc_datetime = dateandtime
551    else:
552        zone = -dateandtime.utcoffset().total_seconds() / 3600.0  # type: ignore
553        utc_datetime = dateandtime.astimezone(pytz.utc)
554
555    timenow = (
556        utc_datetime.hour
557        + (utc_datetime.minute / 60.0)
558        + (utc_datetime.second / 3600.0)
559    )
560
561    JD = julianday(dateandtime)
562    t = jday_to_jcentury(JD + timenow / 24.0)
563    solarDec = sun_declination(t)
564    eqtime = eq_of_time(t)
565
566    solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone)
567    trueSolarTime = (
568        dateandtime.hour * 60.0
569        + dateandtime.minute
570        + dateandtime.second / 60.0
571        + solarTimeFix
572    )
573    #    in minutes as a float, fractional part is seconds
574
575    while trueSolarTime > 1440:
576        trueSolarTime = trueSolarTime - 1440
577
578    hourangle = trueSolarTime / 4.0 - 180.0
579    #    Thanks to Louis Schwarzmayr for the next line:
580    if hourangle < -180:
581        hourangle = hourangle + 360.0
582
583    harad = radians(hourangle)
584
585    csz = sin(radians(latitude)) * sin(radians(solarDec)) + cos(
586        radians(latitude)
587    ) * cos(radians(solarDec)) * cos(harad)
588
589    if csz > 1.0:
590        csz = 1.0
591    elif csz < -1.0:
592        csz = -1.0
593
594    zenith = degrees(acos(csz))
595
596    azDenom = cos(radians(latitude)) * sin(radians(zenith))
597
598    if abs(azDenom) > 0.001:
599        azRad = (
600            (sin(radians(latitude)) * cos(radians(zenith))) - sin(radians(solarDec))
601        ) / azDenom
602
603        if abs(azRad) > 1.0:
604            if azRad < 0:
605                azRad = -1.0
606            else:
607                azRad = 1.0
608
609        azimuth = 180.0 - degrees(acos(azRad))
610
611        if hourangle > 0.0:
612            azimuth = -azimuth
613    else:
614        if latitude > 0.0:
615            azimuth = 180.0
616        else:
617            azimuth = 0.0
618
619    if azimuth < 0.0:
620        azimuth = azimuth + 360.0
621
622    if with_refraction:
623        zenith -= refraction_at_zenith(zenith)
624
625    return zenith, azimuth
626
627
628def zenith(
629    observer: Observer,
630    dateandtime: Optional[datetime.datetime] = None,
631    with_refraction: bool = True,
632) -> float:
633    """Calculate the zenith angle of the sun.
634
635    Args:
636        observer:    Observer to calculate the solar zenith for
637        dateandtime: The date and time for which to calculate the angle.
638                     If `dateandtime` is None or is a naive Python datetime
639                     then it is assumed to be in the UTC timezone.
640        with_refraction: If True adjust zenith to take refraction into account
641
642    Returns:
643        The zenith angle in degrees.
644    """
645
646    if dateandtime is None:
647        dateandtime = now(pytz.UTC)
648
649    return zenith_and_azimuth(observer, dateandtime, with_refraction)[0]
650
651
652def azimuth(
653    observer: Observer, dateandtime: Optional[datetime.datetime] = None,
654) -> float:
655    """Calculate the azimuth angle of the sun.
656
657    Args:
658        observer:    Observer to calculate the solar azimuth for
659        dateandtime: The date and time for which to calculate the angle.
660                     If `dateandtime` is None or is a naive Python datetime
661                     then it is assumed to be in the UTC timezone.
662
663    Returns:
664        The azimuth angle in degrees clockwise from North.
665
666    If `dateandtime` is a naive Python datetime then it is assumed to be
667    in the UTC timezone.
668    """
669
670    if dateandtime is None:
671        dateandtime = now(pytz.UTC)
672
673    return zenith_and_azimuth(observer, dateandtime)[1]
674
675
676def elevation(
677    observer: Observer,
678    dateandtime: Optional[datetime.datetime] = None,
679    with_refraction: bool = True,
680) -> float:
681    """Calculate the sun's angle of elevation.
682
683    Args:
684        observer:    Observer to calculate the solar elevation for
685        dateandtime: The date and time for which to calculate the angle.
686                     If `dateandtime` is None or is a naive Python datetime
687                     then it is assumed to be in the UTC timezone.
688        with_refraction: If True adjust elevation to take refraction into account
689
690    Returns:
691        The elevation angle in degrees above the horizon.
692    """
693
694    if dateandtime is None:
695        dateandtime = now(pytz.UTC)
696
697    return 90.0 - zenith(observer, dateandtime, with_refraction)
698
699
700def dawn(
701    observer: Observer,
702    date: Optional[datetime.date] = None,
703    depression: Union[float, Depression] = Depression.CIVIL,
704    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
705) -> datetime.datetime:
706    """Calculate dawn time.
707
708    Args:
709        observer:   Observer to calculate dawn for
710        date:       Date to calculate for. Default is today's date in the timezone `tzinfo`.
711        depression: Number of degrees below the horizon to use to calculate dawn.
712                    Default is for Civil dawn i.e. 6.0
713        tzinfo:     Timezone to return times in. Default is UTC.
714
715    Returns:
716        Date and time at which dawn occurs.
717
718    Raises:
719        ValueError: if dawn does not occur on the specified date
720    """
721    if isinstance(tzinfo, str):
722        tzinfo = pytz.timezone(tzinfo)
723
724    if date is None:
725        date = today(tzinfo)
726
727    dep: float = 0.0
728    if isinstance(depression, Depression):
729        dep = depression.value
730    else:
731        dep = depression
732
733    try:
734        return time_of_transit(
735            observer, date, 90.0 + dep, SunDirection.RISING
736        ).astimezone(tzinfo)
737    except ValueError as exc:
738        if exc.args[0] == "math domain error":
739            raise ValueError(
740                f"Sun never reaches {dep} degrees below the horizon, at this location."
741            ) from exc
742        else:
743            raise
744
745
746def sunrise(
747    observer: Observer,
748    date: Optional[datetime.date] = None,
749    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
750) -> datetime.datetime:
751    """Calculate sunrise time.
752
753    Args:
754        observer: Observer to calculate sunrise for
755        date:     Date to calculate for. Default is today's date in the timezone `tzinfo`.
756        tzinfo:   Timezone to return times in. Default is UTC.
757
758    Returns:
759        Date and time at which sunrise occurs.
760
761    Raises:
762        ValueError: if the sun does not reach the horizon on the specified date
763    """
764    if isinstance(tzinfo, str):
765        tzinfo = pytz.timezone(tzinfo)
766
767    if date is None:
768        date = today(tzinfo)
769
770    try:
771        return time_of_transit(
772            observer, date, 90.0 + SUN_APPARENT_RADIUS, SunDirection.RISING,
773        ).astimezone(tzinfo)
774    except ValueError as exc:
775        if exc.args[0] == "math domain error":
776            z = zenith(observer, noon(observer, date))
777            if z > 90.0:
778                msg = "Sun is always below the horizon on this day, at this location."
779            else:
780                msg = "Sun is always above the horizon on this day, at this location."
781            raise ValueError(msg) from exc
782        else:
783            raise
784
785
786def sunset(
787    observer: Observer,
788    date: Optional[datetime.date] = None,
789    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
790) -> datetime.datetime:
791    """Calculate sunset time.
792
793    Args:
794        observer: Observer to calculate sunset for
795        date:     Date to calculate for. Default is today's date in the timezone `tzinfo`.
796        tzinfo:   Timezone to return times in. Default is UTC.
797
798    Returns:
799        Date and time at which sunset occurs.
800
801    Raises:
802        ValueError: if the sun does not reach the horizon
803    """
804
805    if isinstance(tzinfo, str):
806        tzinfo = pytz.timezone(tzinfo)
807
808    if date is None:
809        date = today(tzinfo)
810
811    try:
812        return time_of_transit(
813            observer, date, 90.0 + SUN_APPARENT_RADIUS, SunDirection.SETTING,
814        ).astimezone(tzinfo)
815    except ValueError as exc:
816        if exc.args[0] == "math domain error":
817            z = zenith(observer, noon(observer, date))
818            if z > 90.0:
819                msg = "Sun is always below the horizon on this day, at this location."
820            else:
821                msg = "Sun is always above the horizon on this day, at this location."
822            raise ValueError(msg) from exc
823        else:
824            raise
825
826
827def dusk(
828    observer: Observer,
829    date: Optional[datetime.date] = None,
830    depression: Union[float, Depression] = Depression.CIVIL,
831    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
832) -> datetime.datetime:
833    """Calculate dusk time.
834
835    Args:
836        observer:   Observer to calculate dusk for
837        date:       Date to calculate for. Default is today's date in the timezone `tzinfo`.
838        depression: Number of degrees below the horizon to use to calculate dusk.
839                    Default is for Civil dusk i.e. 6.0
840        tzinfo:     Timezone to return times in. Default is UTC.
841
842    Returns:
843        Date and time at which dusk occurs.
844
845    Raises:
846        ValueError: if dusk does not occur on the specified date
847    """
848
849    if isinstance(tzinfo, str):
850        tzinfo = pytz.timezone(tzinfo)
851
852    if date is None:
853        date = today(tzinfo)
854
855    dep: float = 0.0
856    if isinstance(depression, Depression):
857        dep = depression.value
858    else:
859        dep = depression
860
861    try:
862        return time_of_transit(
863            observer, date, 90.0 + dep, SunDirection.SETTING
864        ).astimezone(tzinfo)
865    except ValueError as exc:
866        if exc.args[0] == "math domain error":
867            raise ValueError(
868                f"Sun never reaches {dep} degrees below the horizon, at this location."
869            ) from exc
870        else:
871            raise
872
873
874def daylight(
875    observer: Observer,
876    date: Optional[datetime.date] = None,
877    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
878) -> Tuple[datetime.datetime, datetime.datetime]:
879    """Calculate daylight start and end times.
880
881    Args:
882        observer: Observer to calculate daylight for
883        date:     Date to calculate for. Default is today's date in the timezone `tzinfo`.
884        tzinfo:   Timezone to return times in. Default is UTC.
885
886    Returns:
887        A tuple of the date and time at which daylight starts and ends.
888
889    Raises:
890        ValueError: if the sun does not rise or does not set
891    """
892    if isinstance(tzinfo, str):
893        tzinfo = pytz.timezone(tzinfo)
894
895    if date is None:
896        date = today(tzinfo)
897
898    start = sunrise(observer, date, tzinfo)
899    end = sunset(observer, date, tzinfo)
900
901    return start, end
902
903
904def night(
905    observer: Observer,
906    date: Optional[datetime.date] = None,
907    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
908) -> Tuple[datetime.datetime, datetime.datetime]:
909    """Calculate night start and end times.
910
911    Night is calculated to be between astronomical dusk on the
912    date specified and astronomical dawn of the next day.
913
914    Args:
915        observer: Observer to calculate night for
916        date:     Date to calculate for. Default is today's date for the
917                  specified tzinfo.
918        tzinfo:   Timezone to return times in. Default is UTC.
919
920    Returns:
921        A tuple of the date and time at which night starts and ends.
922
923    Raises:
924        ValueError: if dawn does not occur on the specified date or
925                    dusk on the following day
926    """
927    if isinstance(tzinfo, str):
928        tzinfo = pytz.timezone(tzinfo)
929
930    if date is None:
931        date = today(tzinfo)
932
933    start = dusk(observer, date, 6, tzinfo)
934    tomorrow = date + datetime.timedelta(days=1)
935    end = dawn(observer, tomorrow, 6, tzinfo)
936
937    return start, end
938
939
940def twilight(
941    observer: Observer,
942    date: Optional[datetime.date] = None,
943    direction: SunDirection = SunDirection.RISING,
944    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
945) -> Tuple[datetime.datetime, datetime.datetime]:
946    """Returns the start and end times of Twilight
947    when the sun is traversing in the specified direction.
948
949    This method defines twilight as being between the time
950    when the sun is at -6 degrees and sunrise/sunset.
951
952    Args:
953        observer:  Observer to calculate twilight for
954        date:      Date for which to calculate the times.
955                      Default is today's date in the timezone `tzinfo`.
956        direction: Determines whether the time is for the sun rising or setting.
957                      Use ``astral.SunDirection.RISING`` or ``astral.SunDirection.SETTING``.
958        tzinfo:    Timezone to return times in. Default is UTC.
959
960    Returns:
961        A tuple of the date and time at which twilight starts and ends.
962
963    Raises:
964        ValueError: if the sun does not rise or does not set
965    """
966
967    if isinstance(tzinfo, str):
968        tzinfo = pytz.timezone(tzinfo)
969
970    if date is None:
971        date = today(tzinfo)
972
973    start = time_of_transit(observer, date, 90 + 6, direction).astimezone(tzinfo)
974    if direction == SunDirection.RISING:
975        end = sunrise(observer, date, tzinfo).astimezone(tzinfo)
976    else:
977        end = sunset(observer, date, tzinfo).astimezone(tzinfo)
978
979    if direction == SunDirection.RISING:
980        return start, end
981    else:
982        return end, start
983
984
985def golden_hour(
986    observer: Observer,
987    date: Optional[datetime.date] = None,
988    direction: SunDirection = SunDirection.RISING,
989    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
990) -> Tuple[datetime.datetime, datetime.datetime]:
991    """Returns the start and end times of the Golden Hour
992    when the sun is traversing in the specified direction.
993
994    This method uses the definition from PhotoPills i.e. the
995    golden hour is when the sun is between 4 degrees below the horizon
996    and 6 degrees above.
997
998    Args:
999        observer:  Observer to calculate the golden hour for
1000        date:      Date for which to calculate the times.
1001                      Default is today's date in the timezone `tzinfo`.
1002        direction: Determines whether the time is for the sun rising or setting.
1003                      Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.
1004        tzinfo:    Timezone to return times in. Default is UTC.
1005
1006    Returns:
1007        A tuple of the date and time at which the Golden Hour starts and ends.
1008
1009    Raises:
1010        ValueError: if the sun does not transit the elevations -4 & +6 degrees
1011    """
1012
1013    if isinstance(tzinfo, str):
1014        tzinfo = pytz.timezone(tzinfo)
1015
1016    if date is None:
1017        date = today(tzinfo)
1018
1019    start = time_of_transit(observer, date, 90 + 4, direction).astimezone(tzinfo)
1020    end = time_of_transit(observer, date, 90 - 6, direction).astimezone(tzinfo)
1021
1022    if direction == SunDirection.RISING:
1023        return start, end
1024    else:
1025        return end, start
1026
1027
1028def blue_hour(
1029    observer: Observer,
1030    date: Optional[datetime.date] = None,
1031    direction: SunDirection = SunDirection.RISING,
1032    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
1033) -> Tuple[datetime.datetime, datetime.datetime]:
1034    """Returns the start and end times of the Blue Hour
1035    when the sun is traversing in the specified direction.
1036
1037    This method uses the definition from PhotoPills i.e. the
1038    blue hour is when the sun is between 6 and 4 degrees below the horizon.
1039
1040    Args:
1041        observer:  Observer to calculate the blue hour for
1042        date:      Date for which to calculate the times.
1043                      Default is today's date in the timezone `tzinfo`.
1044        direction: Determines whether the time is for the sun rising or setting.
1045                      Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.
1046        tzinfo:    Timezone to return times in. Default is UTC.
1047
1048    Returns:
1049        A tuple of the date and time at which the Blue Hour starts and ends.
1050
1051    Raises:
1052        ValueError: if the sun does not transit the elevations -4 & -6 degrees
1053    """
1054
1055    if isinstance(tzinfo, str):
1056        tzinfo = pytz.timezone(tzinfo)
1057
1058    if date is None:
1059        date = today(tzinfo)
1060
1061    start = time_of_transit(observer, date, 90 + 6, direction).astimezone(tzinfo)
1062    end = time_of_transit(observer, date, 90 + 4, direction).astimezone(tzinfo)
1063
1064    if direction == SunDirection.RISING:
1065        return start, end
1066    else:
1067        return end, start
1068
1069
1070def rahukaalam(
1071    observer: Observer,
1072    date: Optional[datetime.date] = None,
1073    daytime: bool = True,
1074    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
1075) -> Tuple[datetime.datetime, datetime.datetime]:
1076    """Calculate ruhakaalam times.
1077
1078    Args:
1079        observer: Observer to calculate rahukaalam for
1080        date:     Date to calculate for. Default is today's date in the timezone `tzinfo`.
1081        daytime:  If True calculate for the day time else calculate for the night time.
1082        tzinfo:   Timezone to return times in. Default is UTC.
1083
1084    Returns:
1085        Tuple containing the start and end times for Rahukaalam.
1086
1087    Raises:
1088        ValueError: if the sun does not rise or does not set
1089    """
1090
1091    if isinstance(tzinfo, str):
1092        tzinfo = pytz.timezone(tzinfo)
1093
1094    if date is None:
1095        date = today(tzinfo)
1096
1097    if daytime:
1098        start = sunrise(observer, date, tzinfo)
1099        end = sunset(observer, date, tzinfo)
1100    else:
1101        start = sunset(observer, date, tzinfo)
1102        oneday = datetime.timedelta(days=1)
1103        end = sunrise(observer, date + oneday, tzinfo)
1104
1105    octant_duration = datetime.timedelta(seconds=(end - start).seconds / 8)
1106
1107    # Mo,Sa,Fr,We,Th,Tu,Su
1108    octant_index = [1, 6, 4, 5, 3, 2, 7]
1109
1110    weekday = date.weekday()
1111    octant = octant_index[weekday]
1112
1113    start = start + (octant_duration * octant)
1114    end = start + octant_duration
1115
1116    return start, end
1117
1118
1119def sun(
1120    observer: Observer,
1121    date: Optional[datetime.date] = None,
1122    dawn_dusk_depression: Union[float, Depression] = Depression.CIVIL,
1123    tzinfo: Union[str, datetime.tzinfo] = pytz.utc,
1124) -> Dict:
1125    """Calculate all the info for the sun at once.
1126
1127    Args:
1128        observer:             Observer for which to calculate the times of the sun
1129        date:                 Date to calculate for.
1130                              Default is today's date in the timezone `tzinfo`.
1131        dawn_dusk_depression: Depression to use to calculate dawn and dusk.
1132                              Default is for Civil dusk i.e. 6.0
1133        tzinfo:               Timezone to return times in. Default is UTC.
1134
1135    Returns:
1136        Dictionary with keys ``dawn``, ``sunrise``, ``noon``, ``sunset`` and ``dusk``
1137        whose values are the results of the corresponding functions.
1138
1139    Raises:
1140        ValueError: if passed through from any of the functions
1141    """
1142
1143    if isinstance(tzinfo, str):
1144        tzinfo = pytz.timezone(tzinfo)
1145
1146    if date is None:
1147        date = today(tzinfo)
1148
1149    return {
1150        "dawn": dawn(observer, date, dawn_dusk_depression, tzinfo),
1151        "sunrise": sunrise(observer, date, tzinfo),
1152        "noon": noon(observer, date, tzinfo),
1153        "sunset": sunset(observer, date, tzinfo),
1154        "dusk": dusk(observer, date, dawn_dusk_depression, tzinfo),
1155    }
1156