A library that computes the ephemerides.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

279 lignes
9.4 KiB

  1. #!/usr/bin/env python3
  2. # Kosmorro - Compute The Next Ephemerides
  3. # Copyright (C) 2019 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 as date_type
  18. from skyfield.errors import EphemerisRangeError
  19. from skyfield.timelib import Time
  20. from skyfield.searchlib import find_discrete, find_maxima, find_minima
  21. from numpy import pi
  22. from .data import Event, Star, Planet, ASTERS
  23. from .dateutil import translate_to_timezone
  24. from .enum import EventType
  25. from .exceptions import OutOfRangeDateError
  26. from .core import get_timescale, get_skf_objects, flatten_list
  27. def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  28. earth = get_skf_objects()["earth"]
  29. aster1 = None
  30. aster2 = None
  31. def is_in_conjunction(time: Time):
  32. earth_pos = earth.at(time)
  33. _, aster1_lon, _ = (
  34. earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon()
  35. )
  36. _, aster2_lon, _ = (
  37. earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon()
  38. )
  39. return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype(
  40. "int8"
  41. ) == 0
  42. is_in_conjunction.rough_period = 60.0
  43. computed = []
  44. conjunctions = []
  45. for aster1 in ASTERS:
  46. # Ignore the Sun
  47. if isinstance(aster1, Star):
  48. continue
  49. for aster2 in ASTERS:
  50. if isinstance(aster2, Star) or aster2 == aster1 or aster2 in computed:
  51. continue
  52. times, is_conjs = find_discrete(start_time, end_time, is_in_conjunction)
  53. for i, time in enumerate(times):
  54. if is_conjs[i]:
  55. aster1_pos = (aster1.get_skyfield_object() - earth).at(time)
  56. aster2_pos = (aster2.get_skyfield_object() - earth).at(time)
  57. distance = aster1_pos.separation_from(aster2_pos).degrees
  58. if distance - aster2.get_apparent_radius(
  59. time, earth
  60. ) < aster1.get_apparent_radius(time, earth):
  61. occulting_aster = (
  62. [aster1, aster2]
  63. if aster1_pos.distance().km < aster2_pos.distance().km
  64. else [aster2, aster1]
  65. )
  66. conjunctions.append(
  67. Event(
  68. EventType.OCCULTATION,
  69. occulting_aster,
  70. translate_to_timezone(time.utc_datetime(), timezone),
  71. )
  72. )
  73. else:
  74. conjunctions.append(
  75. Event(
  76. EventType.CONJUNCTION,
  77. [aster1, aster2],
  78. translate_to_timezone(time.utc_datetime(), timezone),
  79. )
  80. )
  81. computed.append(aster1)
  82. return conjunctions
  83. def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  84. earth = get_skf_objects()["earth"]
  85. sun = get_skf_objects()["sun"]
  86. aster = None
  87. def is_oppositing(time: Time) -> [bool]:
  88. earth_pos = earth.at(time)
  89. sun_pos = earth_pos.observe(
  90. sun
  91. ).apparent() # Never do this without eyes protection!
  92. aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent()
  93. _, lon1, _ = sun_pos.ecliptic_latlon()
  94. _, lon2, _ = aster_pos.ecliptic_latlon()
  95. return (lon1.degrees - lon2.degrees) > 180
  96. is_oppositing.rough_period = 1.0
  97. events = []
  98. for aster in ASTERS:
  99. if not isinstance(aster, Planet) or aster.skyfield_name in ["MERCURY", "VENUS"]:
  100. continue
  101. times, _ = find_discrete(start_time, end_time, is_oppositing)
  102. for time in times:
  103. events.append(
  104. Event(
  105. EventType.OPPOSITION,
  106. [aster],
  107. translate_to_timezone(time.utc_datetime(), timezone),
  108. )
  109. )
  110. return events
  111. def _search_maximal_elongations(
  112. start_time: Time, end_time: Time, timezone: int
  113. ) -> [Event]:
  114. earth = get_skf_objects()["earth"]
  115. sun = get_skf_objects()["sun"]
  116. aster = None
  117. def get_elongation(time: Time):
  118. sun_pos = (sun - earth).at(time)
  119. aster_pos = (aster.get_skyfield_object() - earth).at(time)
  120. separation = sun_pos.separation_from(aster_pos)
  121. return separation.degrees
  122. get_elongation.rough_period = 1.0
  123. events = []
  124. for aster in ASTERS:
  125. if aster.skyfield_name not in ["MERCURY", "VENUS"]:
  126. continue
  127. times, elongations = find_maxima(
  128. start_time, end_time, f=get_elongation, epsilon=1.0 / 24 / 3600, num=12
  129. )
  130. for i, time in enumerate(times):
  131. elongation = elongations[i]
  132. events.append(
  133. Event(
  134. EventType.MAXIMAL_ELONGATION,
  135. [aster],
  136. translate_to_timezone(time.utc_datetime(), timezone),
  137. details="{:.3n}°".format(elongation),
  138. )
  139. )
  140. return events
  141. def _get_moon_distance():
  142. earth = get_skf_objects()["earth"]
  143. moon = get_skf_objects()["moon"]
  144. def get_distance(time: Time):
  145. earth_pos = earth.at(time)
  146. moon_pos = earth_pos.observe(moon).apparent()
  147. return moon_pos.distance().au
  148. get_distance.rough_period = 1.0
  149. return get_distance
  150. def _search_moon_apogee(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  151. moon = ASTERS[1]
  152. events = []
  153. times, _ = find_maxima(
  154. start_time, end_time, f=_get_moon_distance(), epsilon=1.0 / 24 / 60
  155. )
  156. for time in times:
  157. events.append(
  158. Event(
  159. EventType.MOON_APOGEE,
  160. [moon],
  161. translate_to_timezone(time.utc_datetime(), timezone),
  162. )
  163. )
  164. return events
  165. def _search_moon_perigee(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  166. moon = ASTERS[1]
  167. events = []
  168. times, _ = find_minima(
  169. start_time, end_time, f=_get_moon_distance(), epsilon=1.0 / 24 / 60
  170. )
  171. for time in times:
  172. events.append(
  173. Event(
  174. EventType.MOON_PERIGEE,
  175. [moon],
  176. translate_to_timezone(time.utc_datetime(), timezone),
  177. )
  178. )
  179. return events
  180. def get_events(date: date_type, timezone: int = 0) -> [Event]:
  181. """Calculate and return a list of events for the given date, adjusted to the given timezone if any.
  182. Find events that happen on April 4th, 2020 (show hours in UTC):
  183. >>> get_events(date_type(2020, 4, 4))
  184. [<Event type=CONJUNCTION objects=[<Object type=planet name=Mercury />, <Object type=planet name=Neptune />] start=2020-04-04 01:14:39.063308+00:00 end=None details=None />]
  185. Find events that happen on April 4th, 2020 (show timezones in UTC+2):
  186. >>> get_events(date_type(2020, 4, 4), 2)
  187. [<Event type=CONJUNCTION objects=[<Object type=planet name=Mercury />, <Object type=planet name=Neptune />] start=2020-04-04 03:14:39.063267+02:00 end=None details=None />]
  188. Find events that happen on April 3rd, 2020 (show timezones in UTC-2):
  189. >>> get_events(date_type(2020, 4, 3), -2)
  190. [<Event type=CONJUNCTION objects=[<Object type=planet name=Mercury />, <Object type=planet name=Neptune />] start=2020-04-03 23:14:39.063388-02:00 end=None details=None />]
  191. :param date: the date for which the events must be calculated
  192. :param timezone: the timezone to adapt the results to. If not given, defaults to 0.
  193. :return: a list of events found for the given date.
  194. """
  195. start_time = get_timescale().utc(date.year, date.month, date.day, -timezone)
  196. end_time = get_timescale().utc(date.year, date.month, date.day + 1, -timezone)
  197. try:
  198. found_events = []
  199. for fun in [
  200. _search_oppositions,
  201. _search_conjunction,
  202. _search_maximal_elongations,
  203. _search_moon_apogee,
  204. _search_moon_perigee,
  205. ]:
  206. found_events.append(fun(start_time, end_time, timezone))
  207. return sorted(flatten_list(found_events), key=lambda event: event.start_time)
  208. except EphemerisRangeError as error:
  209. start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone)
  210. end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone)
  211. start_date = date_type(start_date.year, start_date.month, start_date.day)
  212. end_date = date_type(end_date.year, end_date.month, end_date.day)
  213. raise OutOfRangeDateError(start_date, end_date) from error