A library that computes the ephemerides.
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 

340 rindas
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, SeasonType
  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={ 'deg': 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, 6, 20), get_timescale().utc(2020, 6, 21), 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. events = []
  203. t, y = almanac.find_discrete(start_time,end_time,almanac.seasons(get_skf_objects()))
  204. if len(t) == 0:
  205. return []
  206. else:
  207. events.append(Event(
  208. EventType.SEASON_CHANGE,
  209. [],
  210. translate_to_timezone(t.utc_datetime()[0], timezone)),
  211. details={'season':SeasonType(y[0])} )
  212. return events
  213. def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]:
  214. """Calculate and return a list of events for the given date, adjusted to the given timezone if any.
  215. Find events that happen on April 4th, 2020 (show hours in UTC):
  216. >>> get_events(date(2020, 4, 4))
  217. [<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 />]
  218. Find events that happen on April 4th, 2020 (show timezones in UTC+2):
  219. >>> get_events(date(2020, 4, 4), 2)
  220. [<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 />]
  221. Find events that happen on April 3rd, 2020 (show timezones in UTC-2):
  222. >>> get_events(date(2020, 4, 3), -2)
  223. [<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 />]
  224. If there is no events for the given date, then an empty list is returned:
  225. >>> get_events(date(2021, 3, 20))
  226. []
  227. :param for_date: the date for which the events must be calculated
  228. :param timezone: the timezone to adapt the results to. If not given, defaults to 0.
  229. :return: a list of events found for the given date.
  230. """
  231. start_time = get_timescale().utc(
  232. for_date.year, for_date.month, for_date.day, -timezone
  233. )
  234. end_time = get_timescale().utc(
  235. for_date.year, for_date.month, for_date.day + 1, -timezone
  236. )
  237. try:
  238. found_events = []
  239. for fun in [
  240. _search_oppositions,
  241. _search_conjunction,
  242. _search_maximal_elongations,
  243. _search_moon_apogee,
  244. _search_moon_perigee,
  245. _earth_change_season
  246. ]:
  247. found_events.append(fun(start_time, end_time, timezone))
  248. return sorted(flatten_list(found_events), key=lambda event: event.start_time)
  249. except EphemerisRangeError as error:
  250. start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone)
  251. end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone)
  252. start_date = date(start_date.year, start_date.month, start_date.day)
  253. end_date = date(end_date.year, end_date.month, end_date.day)
  254. raise OutOfRangeDateError(start_date, end_date) from error