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.
 
 

335 lignes
11 KiB

  1. #!/usr/bin/env python3
  2. from datetime import date
  3. from skyfield.errors import EphemerisRangeError
  4. from skyfield.timelib import Time
  5. from skyfield.searchlib import find_discrete, find_maxima, find_minima
  6. from numpy import pi
  7. from skyfield import almanac
  8. from skyfield import api
  9. from .model import Event, Star, Planet, ASTERS
  10. from .dateutil import translate_to_timezone
  11. from .enum import EventType, ObjectIdentifier
  12. from .exceptions import OutOfRangeDateError
  13. from .core import get_timescale, get_skf_objects, flatten_list
  14. def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  15. earth = get_skf_objects()["earth"]
  16. aster1 = None
  17. aster2 = None
  18. def is_in_conjunction(time: Time):
  19. earth_pos = earth.at(time)
  20. _, aster1_lon, _ = (
  21. earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon()
  22. )
  23. _, aster2_lon, _ = (
  24. earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon()
  25. )
  26. return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype(
  27. "int8"
  28. ) == 0
  29. is_in_conjunction.rough_period = 60.0
  30. computed = []
  31. conjunctions = []
  32. for aster1 in ASTERS:
  33. # Ignore the Sun
  34. if isinstance(aster1, Star):
  35. continue
  36. for aster2 in ASTERS:
  37. if isinstance(aster2, Star) or aster2 == aster1 or aster2 in computed:
  38. continue
  39. times, is_conjs = find_discrete(start_time, end_time, is_in_conjunction)
  40. for i, time in enumerate(times):
  41. if is_conjs[i]:
  42. aster1_pos = (aster1.get_skyfield_object() - earth).at(time)
  43. aster2_pos = (aster2.get_skyfield_object() - earth).at(time)
  44. distance = aster1_pos.separation_from(aster2_pos).degrees
  45. if distance - aster2.get_apparent_radius(
  46. time, earth
  47. ) < aster1.get_apparent_radius(time, earth):
  48. occulting_aster = (
  49. [aster1, aster2]
  50. if aster1_pos.distance().km < aster2_pos.distance().km
  51. else [aster2, aster1]
  52. )
  53. conjunctions.append(
  54. Event(
  55. EventType.OCCULTATION,
  56. occulting_aster,
  57. translate_to_timezone(time.utc_datetime(), timezone),
  58. )
  59. )
  60. else:
  61. conjunctions.append(
  62. Event(
  63. EventType.CONJUNCTION,
  64. [aster1, aster2],
  65. translate_to_timezone(time.utc_datetime(), timezone),
  66. )
  67. )
  68. computed.append(aster1)
  69. return conjunctions
  70. def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  71. """Function to search oppositions.
  72. **Warning:** this is an internal function, not intended for use by end-developers.
  73. Will return Mars opposition on 2020-10-13:
  74. >>> oppositions = _search_oppositions(get_timescale().utc(2020, 10, 13), get_timescale().utc(2020, 10, 14), 0)
  75. >>> len(oppositions)
  76. 1
  77. >>> oppositions[0].objects[0]
  78. <Object type=PLANET name=MARS />
  79. Will return nothing if no opposition happens:
  80. >>> _search_oppositions(get_timescale().utc(2021, 3, 20), get_timescale().utc(2021, 3, 21), 0)
  81. []
  82. """
  83. earth = get_skf_objects()["earth"]
  84. sun = get_skf_objects()["sun"]
  85. aster = None
  86. def is_oppositing(time: Time) -> [bool]:
  87. diff = get_angle(time)
  88. return diff > 180
  89. def get_angle(time: Time):
  90. earth_pos = earth.at(time)
  91. sun_pos = earth_pos.observe(
  92. sun
  93. ).apparent() # Never do this without eyes protection!
  94. aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent()
  95. _, lon1, _ = sun_pos.ecliptic_latlon()
  96. _, lon2, _ = aster_pos.ecliptic_latlon()
  97. return lon1.degrees - lon2.degrees
  98. is_oppositing.rough_period = 1.0
  99. events = []
  100. for aster in ASTERS:
  101. if not isinstance(aster, Planet) or aster.identifier in [
  102. ObjectIdentifier.MERCURY,
  103. ObjectIdentifier.VENUS,
  104. ]:
  105. continue
  106. times, _ = find_discrete(start_time, end_time, is_oppositing)
  107. for time in times:
  108. if get_angle(time) < 0:
  109. # If the angle is negative, then it is actually a false positive.
  110. # Just ignoring it.
  111. continue
  112. events.append(
  113. Event(
  114. EventType.OPPOSITION,
  115. [aster],
  116. translate_to_timezone(time.utc_datetime(), timezone),
  117. )
  118. )
  119. return events
  120. def _search_maximal_elongations(
  121. start_time: Time, end_time: Time, timezone: int
  122. ) -> [Event]:
  123. earth = get_skf_objects()["earth"]
  124. sun = get_skf_objects()["sun"]
  125. aster = None
  126. def get_elongation(time: Time):
  127. sun_pos = (sun - earth).at(time)
  128. aster_pos = (aster.get_skyfield_object() - earth).at(time)
  129. separation = sun_pos.separation_from(aster_pos)
  130. return separation.degrees
  131. get_elongation.rough_period = 1.0
  132. events = []
  133. for aster in ASTERS:
  134. if aster.skyfield_name not in ["MERCURY", "VENUS"]:
  135. continue
  136. times, elongations = find_maxima(
  137. start_time, end_time, f=get_elongation, epsilon=1.0 / 24 / 3600, num=12
  138. )
  139. for i, time in enumerate(times):
  140. elongation = elongations[i]
  141. events.append(
  142. Event(
  143. EventType.MAXIMAL_ELONGATION,
  144. [aster],
  145. translate_to_timezone(time.utc_datetime(), timezone),
  146. details="{:.3n}°".format(elongation),
  147. )
  148. )
  149. return events
  150. def _get_moon_distance():
  151. earth = get_skf_objects()["earth"]
  152. moon = get_skf_objects()["moon"]
  153. def get_distance(time: Time):
  154. earth_pos = earth.at(time)
  155. moon_pos = earth_pos.observe(moon).apparent()
  156. return moon_pos.distance().au
  157. get_distance.rough_period = 1.0
  158. return get_distance
  159. def _search_moon_apogee(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  160. moon = ASTERS[1]
  161. events = []
  162. times, _ = find_maxima(
  163. start_time, end_time, f=_get_moon_distance(), epsilon=1.0 / 24 / 60
  164. )
  165. for time in times:
  166. events.append(
  167. Event(
  168. EventType.MOON_APOGEE,
  169. [moon],
  170. translate_to_timezone(time.utc_datetime(), timezone),
  171. )
  172. )
  173. return events
  174. def _search_moon_perigee(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  175. moon = ASTERS[1]
  176. events = []
  177. times, _ = find_minima(
  178. start_time, end_time, f=_get_moon_distance(), epsilon=1.0 / 24 / 60
  179. )
  180. for time in times:
  181. events.append(
  182. Event(
  183. EventType.MOON_PERIGEE,
  184. [moon],
  185. translate_to_timezone(time.utc_datetime(), timezone),
  186. )
  187. )
  188. return events
  189. def _search_earth_season_change(start_time: Time, end_time: Time, timezone: int) -> [Event]:
  190. """Function to find earth season change event.
  191. **Warning:** this is an internal function, not intended for use by end-developers.
  192. Will return Summer Solstice and Autumnal Equinox on respectively 2020-06-20 and 2020-09-21:
  193. >>> season_change = _search_earth_season_change(get_timescale().utc(2020, 5, 13), get_timescale().utc(2020, 10, 14), 0)
  194. >>> len(season_change)
  195. 2
  196. >>> oppositions[0].objects[0]
  197. 'Summer Solstice'
  198. Will return nothing if there is no season change event in the period of time being calculated:
  199. >>> _search_oppositions(get_timescale().utc(2021, 3, 20), get_timescale().utc(2021, 3, 21), 0)
  200. []
  201. """
  202. eph = api.load('de421.bsp')
  203. event = []
  204. t, y = almanac.find_discrete(start_time,end_time,almanac.seasons(eph))
  205. for yi, ti in zip(y,t):
  206. event.append(Event(
  207. EventType[f'{almanac.SEASON_EVENTS[yi].upper().replace(" ","_")}'],
  208. [almanac.SEASON_EVENTS[yi]],
  209. ti.utc_iso(' ')))
  210. return event
  211. def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]:
  212. """Calculate and return a list of events for the given date, adjusted to the given timezone if any.
  213. Find events that happen on April 4th, 2020 (show hours in UTC):
  214. >>> get_events(date(2020, 4, 4))
  215. [<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 />]
  216. Find events that happen on April 4th, 2020 (show timezones in UTC+2):
  217. >>> get_events(date(2020, 4, 4), 2)
  218. [<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 />]
  219. Find events that happen on April 3rd, 2020 (show timezones in UTC-2):
  220. >>> get_events(date(2020, 4, 3), -2)
  221. [<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 />]
  222. If there is no events for the given date, then an empty list is returned:
  223. >>> get_events(date(2021, 3, 20))
  224. []
  225. :param for_date: the date for which the events must be calculated
  226. :param timezone: the timezone to adapt the results to. If not given, defaults to 0.
  227. :return: a list of events found for the given date.
  228. """
  229. start_time = get_timescale().utc(
  230. for_date.year, for_date.month, for_date.day, -timezone
  231. )
  232. end_time = get_timescale().utc(
  233. for_date.year, for_date.month, for_date.day + 1, -timezone
  234. )
  235. try:
  236. found_events = []
  237. for fun in [
  238. _search_oppositions,
  239. _search_conjunction,
  240. _search_maximal_elongations,
  241. _search_moon_apogee,
  242. _search_moon_perigee,
  243. _earth_change_season
  244. ]:
  245. found_events.append(fun(start_time, end_time, timezone))
  246. return sorted(flatten_list(found_events), key=lambda event: event.start_time)
  247. except EphemerisRangeError as error:
  248. start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone)
  249. end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone)
  250. start_date = date(start_date.year, start_date.month, start_date.day)
  251. end_date = date(end_date.year, end_date.month, end_date.day)
  252. raise OutOfRangeDateError(start_date, end_date) from error