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