A library that computes the ephemerides.
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

279 righe
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