A library that computes the ephemerides.
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.
 
 

358 lines
21 KiB

  1. #!/usr/bin/env python3
  2. # Kosmorrolib - The Library To Compute Your Ephemerides
  3. # Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. from datetime import date, datetime, timedelta
  18. from typing import Union
  19. from skyfield.searchlib import find_discrete, find_maxima
  20. from skyfield.timelib import Time
  21. from skyfield.constants import tau
  22. from skyfield.errors import EphemerisRangeError
  23. from .model import Position, AsterEphemerides, MoonPhase, Object, ASTERS
  24. from .dateutil import translate_to_utc_offset, normalize_datetime
  25. from .core import (
  26. get_skf_objects,
  27. get_timescale,
  28. get_iau2000b,
  29. deprecated,
  30. alert_deprecation,
  31. )
  32. from .enum import MoonPhaseType
  33. from .exceptions import OutOfRangeDateError
  34. RISEN_ANGLE = -0.8333
  35. def _get_skyfield_to_moon_phase(
  36. times: [Time], vals: [int], now: Time, utc_offset: Union[int, float]
  37. ) -> Union[MoonPhase, None]:
  38. tomorrow = get_timescale().utc(
  39. now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1
  40. )
  41. next_phase_time = None
  42. i = 0
  43. # Find the current moon phase:
  44. for i, time in enumerate(times):
  45. if now.utc_datetime() <= time.utc_datetime():
  46. if time.utc_datetime() >= tomorrow.utc_datetime():
  47. i -= 1
  48. break
  49. current_phase = MoonPhaseType(vals[i])
  50. if current_phase in [
  51. MoonPhaseType.NEW_MOON,
  52. MoonPhaseType.FIRST_QUARTER,
  53. MoonPhaseType.FULL_MOON,
  54. MoonPhaseType.LAST_QUARTER,
  55. ]:
  56. current_phase_time = translate_to_utc_offset(
  57. times[i].utc_datetime(), utc_offset
  58. )
  59. else:
  60. current_phase_time = None
  61. # Find the next moon phase
  62. for j in range(i + 1, len(times)):
  63. if vals[j] in [0, 2, 4, 6]:
  64. next_phase_time = translate_to_utc_offset(
  65. times[j].utc_datetime(), utc_offset
  66. )
  67. break
  68. return MoonPhase(current_phase, current_phase_time, next_phase_time)
  69. def get_moon_phase(
  70. for_date: date = date.today(), utc_offset: Union[int, float] = 0, **argv
  71. ) -> MoonPhase:
  72. """Calculate and return the moon phase for the given date, adjusted to the given UTC offset if any.
  73. Get the moon phase for the 27 March, 2021:
  74. >>> get_moon_phase(date(2021, 3, 27))
  75. <MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 18:48:10.902298+00:00>
  76. When the moon phase is a new moon, a first quarter, a full moon or a last quarter, you get the exact time
  77. of its happening too:
  78. >>> get_moon_phase(datetime(2021, 3, 28))
  79. <MoonPhase phase_type=MoonPhaseType.FULL_MOON time=2021-03-28 18:48:10.902298+00:00 next_phase_date=2021-04-04 10:02:27.393689+00:00>
  80. Get the moon phase for the 27 March, 2021, in UTC+2:
  81. >>> get_moon_phase(date(2021, 3, 27), utc_offset=2)
  82. <MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 20:48:10.902298+02:00>
  83. Note that the `utc_offset` argument was named `timezone` before version 1.1. The old name still works, but will be dropped in the future.
  84. >>> get_moon_phase(date(2021, 3, 27), timezone=2)
  85. <MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 20:48:10.902298+02:00>
  86. Note that the moon phase can only be computed for a date range.
  87. Asking for the moon phase with an out of range date will result in an exception:
  88. >>> get_moon_phase(date(1000, 1, 1))
  89. Traceback (most recent call last):
  90. ...
  91. kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-08-09 and 2053-09-26
  92. """
  93. if argv.get("timezone") is not None:
  94. alert_deprecation(
  95. "'timezone' argument of the get_moon_phase() function is deprecated. Use utc_offset instead."
  96. )
  97. utc_offset = argv.get("timezone")
  98. earth = get_skf_objects()["earth"]
  99. moon = get_skf_objects()["moon"]
  100. sun = get_skf_objects()["sun"]
  101. def moon_phase_at(time: Time):
  102. time._nutation_angles = get_iau2000b(time)
  103. current_earth = earth.at(time)
  104. _, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon("date")
  105. _, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon("date")
  106. return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int)
  107. moon_phase_at.rough_period = 7.0 # one lunar phase per week
  108. today = get_timescale().utc(for_date.year, for_date.month, for_date.day)
  109. start_time = get_timescale().utc(for_date.year, for_date.month, for_date.day - 10)
  110. end_time = get_timescale().utc(for_date.year, for_date.month, for_date.day + 10)
  111. try:
  112. times, phases = find_discrete(start_time, end_time, moon_phase_at)
  113. return _get_skyfield_to_moon_phase(times, phases, today, utc_offset)
  114. except EphemerisRangeError as error:
  115. start = translate_to_utc_offset(error.start_time.utc_datetime(), utc_offset)
  116. end = translate_to_utc_offset(error.end_time.utc_datetime(), utc_offset)
  117. start = date(start.year, start.month, start.day) + timedelta(days=12)
  118. end = date(end.year, end.month, end.day) - timedelta(days=12)
  119. raise OutOfRangeDateError(start, end) from error
  120. def get_ephemerides(
  121. position: Position,
  122. for_date: date = date.today(),
  123. utc_offset: Union[int, float] = 0,
  124. **argv
  125. ) -> [AsterEphemerides]:
  126. """Compute and return the ephemerides for the given position and date, adjusted to the given UTC offset if any.
  127. Compute the ephemerides for July 7th, 2022:
  128. >>> get_ephemerides(Position(36.6794, 4.8555), date(2022, 7, 7))
  129. [<AsterEphemerides rise_time=2022-07-07 04:29:00 culmination_time=2022-07-07 11:46:00 set_time=2022-07-07 19:02:00 aster=<Object type=STAR name=SUN />>,
  130. <AsterEphemerides rise_time=2022-07-07 12:16:00 culmination_time=2022-07-07 18:06:00 set_time=2022-07-07 23:54:00 aster=<Object type=SATELLITE name=MOON />>,
  131. <AsterEphemerides rise_time=2022-07-07 03:36:00 culmination_time=2022-07-07 10:58:00 set_time=2022-07-07 18:20:00 aster=<Object type=PLANET name=MERCURY />>,
  132. <AsterEphemerides rise_time=2022-07-07 02:30:00 culmination_time=2022-07-07 09:44:00 set_time=2022-07-07 16:58:00 aster=<Object type=PLANET name=VENUS />>,
  133. <AsterEphemerides rise_time=2022-07-07 00:05:00 culmination_time=2022-07-07 06:39:00 set_time=2022-07-07 13:14:00 aster=<Object type=PLANET name=MARS />>,
  134. <AsterEphemerides rise_time=2022-07-07 22:59:00 culmination_time=2022-07-07 05:11:00 set_time=2022-07-07 11:20:00 aster=<Object type=PLANET name=JUPITER />>,
  135. <AsterEphemerides rise_time=2022-07-07 21:06:00 culmination_time=2022-07-07 02:29:00 set_time=2022-07-07 07:48:00 aster=<Object type=PLANET name=SATURN />>,
  136. <AsterEphemerides rise_time=2022-07-07 00:47:00 culmination_time=2022-07-07 07:42:00 set_time=2022-07-07 14:38:00 aster=<Object type=PLANET name=URANUS />>,
  137. <AsterEphemerides rise_time=2022-07-07 22:27:00 culmination_time=2022-07-07 04:25:00 set_time=2022-07-07 10:20:00 aster=<Object type=PLANET name=NEPTUNE />>,
  138. <AsterEphemerides rise_time=2022-07-07 19:46:00 culmination_time=2022-07-07 00:41:00 set_time=2022-07-07 05:33:00 aster=<Object type=PLANET name=PLUTO />>]
  139. UTC offset can be optionnaly set to adapt the hours to your location:
  140. >>> get_ephemerides(Position(36.6794, 4.8555), date(2022, 7, 7), utc_offset=2)
  141. [<AsterEphemerides rise_time=2022-07-07 06:29:00 culmination_time=2022-07-07 13:46:00 set_time=2022-07-07 21:02:00 aster=<Object type=STAR name=SUN />>,
  142. <AsterEphemerides rise_time=2022-07-07 14:16:00 culmination_time=2022-07-07 20:06:00 set_time=2022-07-07 01:27:00 aster=<Object type=SATELLITE name=MOON />>,
  143. <AsterEphemerides rise_time=2022-07-07 05:36:00 culmination_time=2022-07-07 12:58:00 set_time=2022-07-07 20:20:00 aster=<Object type=PLANET name=MERCURY />>,
  144. <AsterEphemerides rise_time=2022-07-07 04:30:00 culmination_time=2022-07-07 11:44:00 set_time=2022-07-07 18:58:00 aster=<Object type=PLANET name=VENUS />>,
  145. <AsterEphemerides rise_time=2022-07-07 02:05:00 culmination_time=2022-07-07 08:39:00 set_time=2022-07-07 15:14:00 aster=<Object type=PLANET name=MARS />>,
  146. <AsterEphemerides rise_time=2022-07-07 01:02:00 culmination_time=2022-07-07 07:11:00 set_time=2022-07-07 13:20:00 aster=<Object type=PLANET name=JUPITER />>,
  147. <AsterEphemerides rise_time=2022-07-07 23:06:00 culmination_time=2022-07-07 04:29:00 set_time=2022-07-07 09:48:00 aster=<Object type=PLANET name=SATURN />>,
  148. <AsterEphemerides rise_time=2022-07-07 02:47:00 culmination_time=2022-07-07 09:42:00 set_time=2022-07-07 16:38:00 aster=<Object type=PLANET name=URANUS />>,
  149. <AsterEphemerides rise_time=2022-07-07 00:31:00 culmination_time=2022-07-07 06:25:00 set_time=2022-07-07 12:20:00 aster=<Object type=PLANET name=NEPTUNE />>,
  150. <AsterEphemerides rise_time=2022-07-07 21:46:00 culmination_time=2022-07-07 02:41:00 set_time=2022-07-07 07:33:00 aster=<Object type=PLANET name=PLUTO />>]
  151. Note that the `utc_offset` argument was named `timezone` before version 1.1. The old name still works, but will be dropped in the future.
  152. >>> get_ephemerides(Position(36.6794, 4.8555), date(2022, 7, 7), timezone=2)
  153. [<AsterEphemerides rise_time=2022-07-07 06:29:00 culmination_time=2022-07-07 13:46:00 set_time=2022-07-07 21:02:00 aster=<Object type=STAR name=SUN />>,
  154. <AsterEphemerides rise_time=2022-07-07 14:16:00 culmination_time=2022-07-07 20:06:00 set_time=2022-07-07 01:27:00 aster=<Object type=SATELLITE name=MOON />>,
  155. <AsterEphemerides rise_time=2022-07-07 05:36:00 culmination_time=2022-07-07 12:58:00 set_time=2022-07-07 20:20:00 aster=<Object type=PLANET name=MERCURY />>,
  156. <AsterEphemerides rise_time=2022-07-07 04:30:00 culmination_time=2022-07-07 11:44:00 set_time=2022-07-07 18:58:00 aster=<Object type=PLANET name=VENUS />>,
  157. <AsterEphemerides rise_time=2022-07-07 02:05:00 culmination_time=2022-07-07 08:39:00 set_time=2022-07-07 15:14:00 aster=<Object type=PLANET name=MARS />>,
  158. <AsterEphemerides rise_time=2022-07-07 01:02:00 culmination_time=2022-07-07 07:11:00 set_time=2022-07-07 13:20:00 aster=<Object type=PLANET name=JUPITER />>,
  159. <AsterEphemerides rise_time=2022-07-07 23:06:00 culmination_time=2022-07-07 04:29:00 set_time=2022-07-07 09:48:00 aster=<Object type=PLANET name=SATURN />>,
  160. <AsterEphemerides rise_time=2022-07-07 02:47:00 culmination_time=2022-07-07 09:42:00 set_time=2022-07-07 16:38:00 aster=<Object type=PLANET name=URANUS />>,
  161. <AsterEphemerides rise_time=2022-07-07 00:31:00 culmination_time=2022-07-07 06:25:00 set_time=2022-07-07 12:20:00 aster=<Object type=PLANET name=NEPTUNE />>,
  162. <AsterEphemerides rise_time=2022-07-07 21:46:00 culmination_time=2022-07-07 02:41:00 set_time=2022-07-07 07:33:00 aster=<Object type=PLANET name=PLUTO />>]
  163. Objects may not rise or set on the given date (e.g. they rise the previous day or set the next day).
  164. In this case, you will get `None` values on the rise or set time.
  165. If an objet does not rise nor set due to your latitude, then both rise and set will be `None`:
  166. >>> north_pole = Position(70, 20)
  167. >>> south_pole = Position(-70, 20)
  168. >>> get_ephemerides(north_pole, date(2021, 6, 20))
  169. [<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>,
  170. <AsterEphemerides rise_time=2021-06-20 14:30:00 culmination_time=2021-06-20 18:44:00 set_time=2021-06-20 22:53:00 aster=<Object type=SATELLITE name=MOON />>,
  171. <AsterEphemerides rise_time=2021-06-20 22:56:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 20:34:00 aster=<Object type=PLANET name=MERCURY />>,
  172. <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>,
  173. <AsterEphemerides rise_time=None culmination_time=2021-06-20 13:17:00 set_time=None aster=<Object type=PLANET name=MARS />>,
  174. <AsterEphemerides rise_time=2021-06-20 23:06:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 06:58:00 aster=<Object type=PLANET name=JUPITER />>,
  175. <AsterEphemerides rise_time=2021-06-20 23:28:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 04:05:00 aster=<Object type=PLANET name=SATURN />>,
  176. <AsterEphemerides rise_time=2021-06-20 21:53:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 17:02:00 aster=<Object type=PLANET name=URANUS />>,
  177. <AsterEphemerides rise_time=2021-06-20 22:51:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 09:50:00 aster=<Object type=PLANET name=NEPTUNE />>,
  178. <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>]
  179. >>> get_ephemerides(north_pole, date(2021, 12, 21))
  180. [<AsterEphemerides rise_time=None culmination_time=2021-12-21 10:38:00 set_time=None aster=<Object type=STAR name=SUN />>,
  181. <AsterEphemerides rise_time=None culmination_time=2021-12-21 00:04:00 set_time=None aster=<Object type=SATELLITE name=MOON />>,
  182. <AsterEphemerides rise_time=None culmination_time=2021-12-21 11:33:00 set_time=None aster=<Object type=PLANET name=MERCURY />>,
  183. <AsterEphemerides rise_time=2021-12-21 11:58:00 culmination_time=2021-12-21 12:33:00 set_time=2021-12-21 13:08:00 aster=<Object type=PLANET name=VENUS />>,
  184. <AsterEphemerides rise_time=None culmination_time=2021-12-21 08:54:00 set_time=None aster=<Object type=PLANET name=MARS />>,
  185. <AsterEphemerides rise_time=2021-12-21 11:07:00 culmination_time=2021-12-21 14:43:00 set_time=2021-12-21 18:19:00 aster=<Object type=PLANET name=JUPITER />>,
  186. <AsterEphemerides rise_time=2021-12-21 11:32:00 culmination_time=2021-12-21 13:33:00 set_time=2021-12-21 15:33:00 aster=<Object type=PLANET name=SATURN />>,
  187. <AsterEphemerides rise_time=2021-12-21 09:54:00 culmination_time=2021-12-21 19:13:00 set_time=2021-12-21 04:37:00 aster=<Object type=PLANET name=URANUS />>,
  188. <AsterEphemerides rise_time=2021-12-21 10:49:00 culmination_time=2021-12-21 16:05:00 set_time=2021-12-21 21:21:00 aster=<Object type=PLANET name=NEPTUNE />>,
  189. <AsterEphemerides rise_time=None culmination_time=2021-12-21 12:31:00 set_time=None aster=<Object type=PLANET name=PLUTO />>]
  190. >>> get_ephemerides(south_pole, date(2021, 6, 20))
  191. [<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>,
  192. <AsterEphemerides rise_time=2021-06-20 11:10:00 culmination_time=2021-06-20 19:06:00 set_time=2021-06-20 01:20:00 aster=<Object type=SATELLITE name=MOON />>,
  193. <AsterEphemerides rise_time=2021-06-20 07:47:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 11:48:00 aster=<Object type=PLANET name=MERCURY />>,
  194. <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>,
  195. <AsterEphemerides rise_time=2021-06-20 12:14:00 culmination_time=2021-06-20 13:17:00 set_time=2021-06-20 14:21:00 aster=<Object type=PLANET name=MARS />>,
  196. <AsterEphemerides rise_time=2021-06-20 18:32:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 11:32:00 aster=<Object type=PLANET name=JUPITER />>,
  197. <AsterEphemerides rise_time=2021-06-20 15:20:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 12:12:00 aster=<Object type=PLANET name=SATURN />>,
  198. <AsterEphemerides rise_time=2021-06-20 04:32:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 10:26:00 aster=<Object type=PLANET name=URANUS />>,
  199. <AsterEphemerides rise_time=2021-06-20 21:28:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 11:13:00 aster=<Object type=PLANET name=NEPTUNE />>,
  200. <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>]
  201. >>> get_ephemerides(south_pole, date(2021, 12, 22))
  202. [<AsterEphemerides rise_time=None culmination_time=2021-12-22 10:39:00 set_time=None aster=<Object type=STAR name=SUN />>,
  203. <AsterEphemerides rise_time=None culmination_time=2021-12-22 01:01:00 set_time=None aster=<Object type=SATELLITE name=MOON />>,
  204. <AsterEphemerides rise_time=None culmination_time=2021-12-22 11:35:00 set_time=None aster=<Object type=PLANET name=MERCURY />>,
  205. <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=VENUS />>,
  206. <AsterEphemerides rise_time=None culmination_time=2021-12-22 08:53:00 set_time=None aster=<Object type=PLANET name=MARS />>,
  207. <AsterEphemerides rise_time=2021-12-22 05:52:00 culmination_time=2021-12-22 14:40:00 set_time=2021-12-22 23:26:00 aster=<Object type=PLANET name=JUPITER />>,
  208. <AsterEphemerides rise_time=2021-12-22 02:41:00 culmination_time=2021-12-22 13:29:00 set_time=2021-12-22 00:21:00 aster=<Object type=PLANET name=SATURN />>,
  209. <AsterEphemerides rise_time=2021-12-22 16:01:00 culmination_time=2021-12-22 19:09:00 set_time=2021-12-22 22:17:00 aster=<Object type=PLANET name=URANUS />>,
  210. <AsterEphemerides rise_time=2021-12-22 08:59:00 culmination_time=2021-12-22 16:01:00 set_time=2021-12-22 23:04:00 aster=<Object type=PLANET name=NEPTUNE />>,
  211. <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=PLUTO />>]
  212. Please note:
  213. - The ephemerides can only be computed for a date range. Asking for the ephemerides with an out of range date will result in an exception:
  214. >>> get_ephemerides(Position(50.5824, 3.0624), date(1000, 1, 1))
  215. Traceback (most recent call last):
  216. ...
  217. kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-29 and 2053-10-07
  218. - The date given in parameter is considered as being given from the point of view of the given UTC offset.
  219. Using a UTC offset that does not correspond to the place's actual one can impact the returned times.
  220. """
  221. if argv.get("timezone") is not None:
  222. alert_deprecation(
  223. "'timezone' argument of the get_ephemerides() function is deprecated. Use utc_offset instead."
  224. )
  225. utc_offset = argv.get("timezone")
  226. def get_angle(for_aster: Object):
  227. def fun(time: Time) -> float:
  228. return (
  229. position.get_planet_topos()
  230. .at(time)
  231. .observe(for_aster.skyfield_object)
  232. .apparent()
  233. .altaz()[0]
  234. .degrees
  235. )
  236. fun.rough_period = 1.0
  237. return fun
  238. def is_risen(for_aster: Object):
  239. def fun(time: Time) -> bool:
  240. return get_angle(for_aster)(time) > RISEN_ANGLE
  241. fun.rough_period = 0.5
  242. return fun
  243. # The date given in argument is supposed to be given in the given UTC offset (more natural for a human),
  244. # but we need it in UTC. Subtracting the offset to get it in UTC.
  245. start_time = get_timescale().utc(
  246. for_date.year, for_date.month, for_date.day, -utc_offset
  247. )
  248. end_time = get_timescale().utc(
  249. for_date.year, for_date.month, for_date.day + 1, -utc_offset
  250. )
  251. ephemerides = []
  252. try:
  253. for aster in ASTERS:
  254. times, risen_info = find_discrete(start_time, end_time, is_risen(aster))
  255. culmination_time, _ = find_maxima(start_time, end_time, get_angle(aster))
  256. rise_time, set_time = None, None
  257. culmination_time = (
  258. culmination_time[0] if len(culmination_time) == 1 else None
  259. )
  260. for i, time in enumerate(times):
  261. time_dt = normalize_datetime(
  262. translate_to_utc_offset(time.utc_datetime(), to_tz=utc_offset)
  263. )
  264. if time_dt is not None and time_dt.day != for_date.day:
  265. continue
  266. if risen_info[i]:
  267. rise_time = time_dt
  268. else:
  269. set_time = time_dt
  270. if culmination_time is not None:
  271. culmination_time = normalize_datetime(
  272. translate_to_utc_offset(
  273. culmination_time.utc_datetime(),
  274. to_tz=utc_offset,
  275. )
  276. )
  277. ephemerides.append(
  278. AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)
  279. )
  280. except EphemerisRangeError as error:
  281. start = translate_to_utc_offset(error.start_time.utc_datetime(), utc_offset)
  282. end = translate_to_utc_offset(error.end_time.utc_datetime(), utc_offset)
  283. start = date(start.year, start.month, start.day + 1)
  284. end = date(end.year, end.month, end.day - 1)
  285. raise OutOfRangeDateError(start, end) from error
  286. return ephemerides