A library that computes the ephemerides.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

678 строки
26 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, timedelta
  18. from typing import Union
  19. from skyfield.errors import EphemerisRangeError
  20. from skyfield.timelib import Time
  21. from skyfield.searchlib import find_discrete, find_maxima, find_minima
  22. from skyfield.units import Angle
  23. from skyfield import almanac, eclipselib
  24. from math import pi
  25. from kosmorrolib.model import (
  26. Event,
  27. Object,
  28. Star,
  29. Planet,
  30. get_aster,
  31. ASTERS,
  32. EARTH,
  33. )
  34. from kosmorrolib.dateutil import translate_to_utc_offset
  35. from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType, LunarEclipseType
  36. from kosmorrolib.exceptions import InvalidDateRangeError, OutOfRangeDateError
  37. from kosmorrolib.core import (
  38. get_timescale,
  39. get_skf_objects,
  40. flatten_list,
  41. deprecated,
  42. alert_deprecation,
  43. )
  44. def _search_conjunctions_occultations(
  45. start_time: Time, end_time: Time, utc_offset: Union[int, float]
  46. ) -> [Event]:
  47. """Function to search conjunction.
  48. **Warning:** this is an internal function, not intended for use by end-developers.
  49. Will return MOON and VENUS opposition on 2021-06-12:
  50. >>> conjunction = _search_conjunctions_occultations(get_timescale().utc(2021, 6, 12), get_timescale().utc(2021, 6, 13), 0)
  51. >>> len(conjunction)
  52. 1
  53. >>> conjunction[0].objects
  54. [<Object type=SATELLITE name=MOON />, <Object type=PLANET name=VENUS />]
  55. Will return nothing if no conjunction happens:
  56. >>> _search_conjunctions_occultations(get_timescale().utc(2021, 6, 17),get_timescale().utc(2021, 6, 18), 0)
  57. []
  58. This function detects occultations too:
  59. >>> _search_conjunctions_occultations(get_timescale().utc(2021, 4, 17),get_timescale().utc(2021, 4, 18), 0)
  60. [<Event type=OCCULTATION objects=[<Object type=SATELLITE name=MOON />, <Object type=PLANET name=MARS />] start=2021-04-17 12:08:16.115650+00:00 end=None details=None />]
  61. """
  62. earth = get_skf_objects()["earth"]
  63. aster1 = None
  64. aster2 = None
  65. def is_in_conjunction(time: Time):
  66. earth_pos = earth.at(time)
  67. _, aster1_lon, _ = (
  68. earth_pos.observe(aster1.skyfield_object).apparent().ecliptic_latlon()
  69. )
  70. _, aster2_lon, _ = (
  71. earth_pos.observe(aster2.skyfield_object).apparent().ecliptic_latlon()
  72. )
  73. return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype(
  74. "int8"
  75. ) == 0
  76. is_in_conjunction.rough_period = 60.0
  77. computed = []
  78. events = []
  79. for aster1 in ASTERS:
  80. # Ignore the Sun
  81. if isinstance(aster1, Star):
  82. continue
  83. for aster2 in ASTERS:
  84. if isinstance(aster2, Star) or aster2 == aster1 or aster2 in computed:
  85. continue
  86. times, is_conjs = find_discrete(start_time, end_time, is_in_conjunction)
  87. for i, time in enumerate(times):
  88. if is_conjs[i]:
  89. aster1_pos = (aster1.skyfield_object - earth).at(time)
  90. aster2_pos = (aster2.skyfield_object - earth).at(time)
  91. distance = aster1_pos.separation_from(aster2_pos).degrees
  92. if (
  93. distance - aster2.get_apparent_radius(time).degrees
  94. < aster1.get_apparent_radius(time).degrees
  95. ):
  96. occulting_aster = (
  97. [aster1, aster2]
  98. if aster1_pos.distance().km < aster2_pos.distance().km
  99. else [aster2, aster1]
  100. )
  101. events.append(
  102. Event(
  103. EventType.OCCULTATION,
  104. occulting_aster,
  105. translate_to_utc_offset(
  106. time.utc_datetime(), utc_offset
  107. ),
  108. )
  109. )
  110. else:
  111. events.append(
  112. Event(
  113. EventType.CONJUNCTION,
  114. [aster1, aster2],
  115. translate_to_utc_offset(
  116. time.utc_datetime(), utc_offset
  117. ),
  118. )
  119. )
  120. computed.append(aster1)
  121. return events
  122. def _search_oppositions(
  123. start_time: Time, end_time: Time, utc_offset: Union[int, float]
  124. ) -> [Event]:
  125. """Function to search oppositions.
  126. **Warning:** this is an internal function, not intended for use by end-developers.
  127. Will return Mars opposition on 2020-10-13:
  128. >>> oppositions = _search_oppositions(get_timescale().utc(2020, 10, 13), get_timescale().utc(2020, 10, 14), 0)
  129. >>> len(oppositions)
  130. 1
  131. >>> oppositions[0].objects[0]
  132. <Object type=PLANET name=MARS />
  133. Will return nothing if no opposition happens:
  134. >>> _search_oppositions(get_timescale().utc(2021, 3, 20), get_timescale().utc(2021, 3, 21), 0)
  135. []
  136. >>> _search_oppositions(get_timescale().utc(2022, 12, 24), get_timescale().utc(2022, 12, 25), 0)
  137. []
  138. """
  139. earth = get_skf_objects()["earth"]
  140. sun = get_skf_objects()["sun"]
  141. aster = None
  142. def is_oppositing(time: Time) -> [bool]:
  143. diff = get_angle(time)
  144. return diff > 180
  145. def get_angle(time: Time):
  146. earth_pos = earth.at(time)
  147. sun_pos = earth_pos.observe(
  148. sun
  149. ).apparent() # Never do this without eyes protection!
  150. aster_pos = earth_pos.observe(aster.skyfield_object).apparent()
  151. _, lon1, _ = sun_pos.ecliptic_latlon()
  152. _, lon2, _ = aster_pos.ecliptic_latlon()
  153. return lon1.degrees - lon2.degrees
  154. is_oppositing.rough_period = 1.0
  155. events = []
  156. for aster in ASTERS:
  157. if not isinstance(aster, Planet) or aster.identifier in [
  158. ObjectIdentifier.MERCURY,
  159. ObjectIdentifier.VENUS,
  160. ]:
  161. continue
  162. times, _ = find_discrete(start_time, end_time, is_oppositing)
  163. for time in times:
  164. if int(get_angle(time)) != 180:
  165. # If the angle is negative, then it is actually a false positive.
  166. # Just ignoring it.
  167. continue
  168. events.append(
  169. Event(
  170. EventType.OPPOSITION,
  171. [aster],
  172. translate_to_utc_offset(time.utc_datetime(), utc_offset),
  173. )
  174. )
  175. return events
  176. def _search_maximal_elongations(
  177. start_time: Time, end_time: Time, utc_offset: Union[int, float]
  178. ) -> [Event]:
  179. """Function to search oppositions.
  180. **Warning:** this is an internal function, not intended for use by end-developers.
  181. Will return Mercury maimum elogation for September 14, 2021:
  182. >>> get_events(date(2021, 9, 14))
  183. [<Event type=MAXIMAL_ELONGATION objects=[<Object type=PLANET name=MERCURY />] start=2021-09-14 04:13:46.664879+00:00 end=None details={'deg': 26.8} />]
  184. """
  185. earth = get_skf_objects()["earth"]
  186. sun = get_skf_objects()["sun"]
  187. def get_elongation(planet: Object):
  188. def f(time: Time):
  189. sun_pos = (sun - earth).at(time)
  190. aster_pos = (planet.skyfield_object - earth).at(time)
  191. separation = sun_pos.separation_from(aster_pos)
  192. return separation.degrees
  193. f.rough_period = 1.0
  194. return f
  195. events = []
  196. for identifier in [ObjectIdentifier.MERCURY, ObjectIdentifier.VENUS]:
  197. planet = get_aster(identifier)
  198. times, elongations = find_maxima(
  199. start_time,
  200. end_time,
  201. f=get_elongation(planet),
  202. epsilon=1.0 / 24 / 3600,
  203. num=12,
  204. )
  205. for i, time in enumerate(times):
  206. elongation = round(elongations[i], 1)
  207. events.append(
  208. Event(
  209. EventType.MAXIMAL_ELONGATION,
  210. [planet],
  211. translate_to_utc_offset(time.utc_datetime(), utc_offset),
  212. details={"deg": float(elongation)},
  213. )
  214. )
  215. return events
  216. def _get_distance(to_aster: Object, from_aster: Object):
  217. def get_distance(time: Time):
  218. from_pos = from_aster.skyfield_object.at(time)
  219. to_pos = from_pos.observe(to_aster.skyfield_object)
  220. return to_pos.distance().km
  221. get_distance.rough_period = 1.0
  222. return get_distance
  223. def _search_apogee(to_aster: Object, from_aster: Object = EARTH) -> callable:
  224. """Search for moon apogee
  225. **Warning:** this is an internal function, not intended for use by end-developers.
  226. Get the moon apogee:
  227. >>> _search_apogee(ASTERS[1])(get_timescale().utc(2021, 6, 8), get_timescale().utc(2021, 6, 9), 0)
  228. [<Event type=APOGEE objects=[<Object type=SATELLITE name=MOON />] start=2021-06-08 02:39:40.165271+00:00 end=None details={'distance_km': 406211.04850197025} />]
  229. Get the Earth's apogee:
  230. >>> _search_apogee(EARTH, from_aster=ASTERS[0])(get_timescale().utc(2021, 7, 5), get_timescale().utc(2021, 7, 6), 0)
  231. [<Event type=APOGEE objects=[<Object type=PLANET name=EARTH />] start=2021-07-05 22:35:42.148792+00:00 end=None details={'distance_km': 152100521.91712126} />]
  232. """
  233. def f(start_time: Time, end_time: Time, utc_offset: Union[int, float]) -> [Event]:
  234. events = []
  235. times, distances = find_maxima(
  236. start_time,
  237. end_time,
  238. f=_get_distance(to_aster, from_aster),
  239. epsilon=1.0 / 24 / 60,
  240. )
  241. for i, time in enumerate(times):
  242. events.append(
  243. Event(
  244. EventType.APOGEE,
  245. [to_aster],
  246. translate_to_utc_offset(time.utc_datetime(), utc_offset),
  247. details={"distance_km": float(distances[i])},
  248. )
  249. )
  250. return events
  251. return f
  252. def _search_perigee(aster: Object, from_aster: Object = EARTH) -> callable:
  253. """Search for moon perigee
  254. **Warning:** this is an internal function, not intended for use by end-developers.
  255. Get the moon perigee:
  256. >>> _search_perigee(ASTERS[1])(get_timescale().utc(2021, 5, 26), get_timescale().utc(2021, 5, 27), 0)
  257. [<Event type=PERIGEE objects=[<Object type=SATELLITE name=MOON />] start=2021-05-26 01:56:01.983455+00:00 end=None details={'distance_km': 357313.9680798693} />]
  258. Get the Earth's perigee:
  259. >>> _search_perigee(EARTH, from_aster=ASTERS[0])(get_timescale().utc(2021, 1, 2), get_timescale().utc(2021, 1, 3), 0)
  260. [<Event type=PERIGEE objects=[<Object type=PLANET name=EARTH />] start=2021-01-02 13:59:00.495905+00:00 end=None details={'distance_km': 147093166.1686309} />]
  261. """
  262. def f(start_time: Time, end_time: Time, utc_offset: Union[int, float]) -> [Event]:
  263. events = []
  264. times, distances = find_minima(
  265. start_time,
  266. end_time,
  267. f=_get_distance(aster, from_aster),
  268. epsilon=1.0 / 24 / 60,
  269. )
  270. for i, time in enumerate(times):
  271. events.append(
  272. Event(
  273. EventType.PERIGEE,
  274. [aster],
  275. translate_to_utc_offset(time.utc_datetime(), utc_offset),
  276. details={"distance_km": float(distances[i])},
  277. )
  278. )
  279. return events
  280. return f
  281. def _search_earth_season_change(
  282. start_time: Time, end_time: Time, utc_offset: Union[int, float]
  283. ) -> [Event]:
  284. """Function to find earth season change event.
  285. **Warning:** this is an internal function, not intended for use by end-developers.
  286. Will return JUNE SOLSTICE on 2020/06/20:
  287. >>> season_change = _search_earth_season_change(get_timescale().utc(2020, 6, 20), get_timescale().utc(2020, 6, 21), 0)
  288. >>> len(season_change)
  289. 1
  290. >>> season_change[0].event_type
  291. <EventType.SEASON_CHANGE: 7>
  292. >>> season_change[0].details
  293. {'season': <SeasonType.JUNE_SOLSTICE: 1>}
  294. Will return nothing if there is no season change event in the period of time being calculated:
  295. >>> _search_earth_season_change(get_timescale().utc(2021, 6, 17), get_timescale().utc(2021, 6, 18), 0)
  296. []
  297. """
  298. events = []
  299. event_time, event_id = almanac.find_discrete(
  300. start_time, end_time, almanac.seasons(get_skf_objects())
  301. )
  302. if len(event_time) == 0:
  303. return []
  304. events.append(
  305. Event(
  306. EventType.SEASON_CHANGE,
  307. [],
  308. translate_to_utc_offset(event_time.utc_datetime()[0], utc_offset),
  309. details={"season": SeasonType(event_id[0])},
  310. )
  311. )
  312. return events
  313. def _search_lunar_eclipse(
  314. start_time: Time, end_time: Time, utc_offset: Union[int, float]
  315. ) -> [Event]:
  316. """Function to detect lunar eclipses.
  317. **Warning:** this is an internal function, not intended for use by end-developers.
  318. >>> _search_lunar_eclipse(get_timescale().utc(2021, 5, 26), get_timescale().utc(2021, 5, 27), 0)
  319. [<Event type=LUNAR_ECLIPSE objects=[<Object type=SATELLITE name=MOON />] start=2021-05-26 08:49:13.314888+00:00 end=2021-05-26 13:48:15.757096+00:00 details={'type': <LunarEclipseType.TOTAL: 2>, 'maximum': datetime.datetime(2021, 5, 26, 11, 18, 42, 328842, tzinfo=datetime.timezone.utc)} />]
  320. >>> _search_lunar_eclipse(get_timescale().utc(2019, 7, 16), get_timescale().utc(2019, 7, 17), 0)
  321. [<Event type=LUNAR_ECLIPSE objects=[<Object type=SATELLITE name=MOON />] start=2019-07-16 18:41:24.400419+00:00 end=2019-07-17 00:20:20.079536+00:00 details={'type': <LunarEclipseType.PARTIAL: 1>, 'maximum': datetime.datetime(2019, 7, 16, 21, 30, 44, 170096, tzinfo=datetime.timezone.utc)} />]
  322. >>> _search_lunar_eclipse(get_timescale().utc(2017, 2, 11), get_timescale().utc(2017, 2, 12), 0)
  323. [<Event type=LUNAR_ECLIPSE objects=[<Object type=SATELLITE name=MOON />] start=2017-02-10 22:04:24.192412+00:00 end=2017-02-11 03:23:42.046415+00:00 details={'type': <LunarEclipseType.PENUMBRAL: 0>, 'maximum': datetime.datetime(2017, 2, 11, 0, 43, 51, 793786, tzinfo=datetime.timezone.utc)} />]
  324. """
  325. moon = get_aster(ObjectIdentifier.MOON)
  326. events = []
  327. t, y, details = eclipselib.lunar_eclipses(start_time, end_time, get_skf_objects())
  328. for ti, yi in zip(t, y):
  329. penumbra_radius = Angle(radians=details["penumbra_radius_radians"][0])
  330. _, max_lon, _ = (
  331. EARTH.skyfield_object.at(ti)
  332. .observe(moon.skyfield_object)
  333. .apparent()
  334. .ecliptic_latlon()
  335. )
  336. def is_in_penumbra(time: Time):
  337. _, lon, _ = (
  338. EARTH.skyfield_object.at(time)
  339. .observe(moon.skyfield_object)
  340. .apparent()
  341. .ecliptic_latlon()
  342. )
  343. moon_radius = details["moon_radius_radians"]
  344. return (
  345. abs(max_lon.radians - lon.radians)
  346. < penumbra_radius.radians + moon_radius
  347. )
  348. is_in_penumbra.rough_period = 60.0
  349. search_start_time = get_timescale().from_datetime(
  350. start_time.utc_datetime() - timedelta(days=1)
  351. )
  352. search_end_time = get_timescale().from_datetime(
  353. end_time.utc_datetime() + timedelta(days=1)
  354. )
  355. eclipse_start, _ = find_discrete(search_start_time, ti, is_in_penumbra)
  356. eclipse_end, _ = find_discrete(ti, search_end_time, is_in_penumbra)
  357. events.append(
  358. Event(
  359. EventType.LUNAR_ECLIPSE,
  360. [moon],
  361. start_time=translate_to_utc_offset(
  362. eclipse_start[0].utc_datetime(), utc_offset
  363. ),
  364. end_time=translate_to_utc_offset(
  365. eclipse_end[0].utc_datetime(), utc_offset
  366. ),
  367. details={
  368. "type": LunarEclipseType(yi),
  369. "maximum": translate_to_utc_offset(ti.utc_datetime(), utc_offset),
  370. },
  371. )
  372. )
  373. return events
  374. def get_events(
  375. for_date: date = date.today(), utc_offset: Union[int, float] = 0, **argv
  376. ) -> [Event]:
  377. """Calculate and return a list of events for the given date, adjusted to the given UTC offset if any.
  378. Find events that happen on April 4th, 2020 (show hours in UTC):
  379. >>> get_events(date(2020, 4, 4))
  380. [<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 />]
  381. Find events that happen on April 4th, 2020 (displayed in UTC+2):
  382. >>> get_events(date(2020, 4, 4), utc_offset=2)
  383. [<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 />]
  384. Find events that happen on April 3rd, 2020 (displayed in UTC-2):
  385. >>> get_events(date(2020, 4, 3), utc_offset=-2)
  386. [<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 />]
  387. 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.
  388. >>> get_events(date(2020, 4, 4), timezone=2)
  389. [<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 />]
  390. If there is no events for the given date, then an empty list is returned:
  391. >>> get_events(date(2021, 4, 20))
  392. []
  393. Note that the events can only be found for a date range.
  394. Asking for the events with an out of range date will result in an exception:
  395. >>> get_events(date(1000, 1, 1))
  396. Traceback (most recent call last):
  397. ...
  398. kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-28 and 2053-10-08
  399. :param for_date: the date for which the events must be calculated
  400. :param utc_offset: the UTC offset to adapt the results to. If not given, defaults to 0.
  401. :return: a list of events found for the given date.
  402. """
  403. if argv.get("timezone") is not None:
  404. alert_deprecation(
  405. "'timezone' argument of the get_events() function is deprecated. Use utc_offset instead."
  406. )
  407. utc_offset = argv.get("timezone")
  408. start_time = get_timescale().utc(
  409. for_date.year, for_date.month, for_date.day, -utc_offset
  410. )
  411. end_time = get_timescale().utc(
  412. for_date.year, for_date.month, for_date.day + 1, -utc_offset
  413. )
  414. try:
  415. found_events = []
  416. for fun in [
  417. _search_oppositions,
  418. _search_conjunctions_occultations,
  419. _search_maximal_elongations,
  420. _search_apogee(ASTERS[1]),
  421. _search_perigee(ASTERS[1]),
  422. _search_apogee(EARTH, from_aster=ASTERS[0]),
  423. _search_perigee(EARTH, from_aster=ASTERS[0]),
  424. _search_earth_season_change,
  425. _search_lunar_eclipse,
  426. ]:
  427. found_events.append(fun(start_time, end_time, utc_offset))
  428. return sorted(flatten_list(found_events), key=lambda event: event.start_time)
  429. except EphemerisRangeError as error:
  430. start_date = translate_to_utc_offset(
  431. error.start_time.utc_datetime(), utc_offset
  432. )
  433. end_date = translate_to_utc_offset(error.end_time.utc_datetime(), utc_offset)
  434. start_date = date(start_date.year, start_date.month, start_date.day)
  435. end_date = date(end_date.year, end_date.month, end_date.day)
  436. raise OutOfRangeDateError(start_date, end_date) from error
  437. def search_events(
  438. event_types: [EventType],
  439. end: date,
  440. start: date = date.today(),
  441. utc_offset: Union[int, float] = 0,
  442. ) -> [Event]:
  443. """Search between `start` and `end` dates, and return a list of matching events for the given time range, adjusted to a given UTC offset.
  444. Find all events between January 27th, 2020 and January 29th, 2020:
  445. >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION, EventType.OCCULTATION, EventType.MAXIMAL_ELONGATION, EventType.APOGEE, EventType.PERIGEE, EventType.SEASON_CHANGE, EventType.LUNAR_ECLIPSE]
  446. >>> search_events(event_types, end=date(2020, 1, 29), start=date(2020, 1, 27)) # doctest: +NORMALIZE_WHITESPACE
  447. [<Event type=CONJUNCTION objects=[<Object type=PLANET name=VENUS />, <Object type=PLANET name=NEPTUNE />] start=2020-01-27 20:00:23.242428+00:00 end=None details=None />,
  448. <Event type=CONJUNCTION objects=[<Object type=SATELLITE name=MOON />, <Object type=PLANET name=NEPTUNE />] start=2020-01-28 09:33:45.000618+00:00 end=None details=None />,
  449. <Event type=CONJUNCTION objects=[<Object type=SATELLITE name=MOON />, <Object type=PLANET name=VENUS />] start=2020-01-28 11:01:51.909499+00:00 end=None details=None />,
  450. <Event type=APOGEE objects=[<Object type=SATELLITE name=MOON />] start=2020-01-29 21:32:13.884314+00:00 end=None details={'distance_km': 405426.4150890029} />]
  451. Find Apogee events between January 27th, 2020 and January 29th, 2020:
  452. >>> search_events([EventType.APOGEE], end=date(2020, 1, 29), start=date(2020, 1, 27))
  453. [<Event type=APOGEE objects=[<Object type=SATELLITE name=MOON />] start=2020-01-29 21:32:13.884314+00:00 end=None details={'distance_km': 405426.4150890029} />]
  454. Find Apogee events between January 27th, 2020 and January 29th, 2020 (show times in UTC-6):
  455. >>> search_events([EventType.APOGEE], end=date(2020, 1, 29), start=date(2020, 1, 27), utc_offset=-6)
  456. [<Event type=APOGEE objects=[<Object type=SATELLITE name=MOON />] start=2020-01-29 15:32:13.884314-06:00 end=None details={'distance_km': 405426.4150890029} />]
  457. If no events occurred in the given time range, an empty list is returned.
  458. >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION, EventType.SEASON_CHANGE, EventType.LUNAR_ECLIPSE]
  459. >>> search_events(event_types, end=date(2021, 5, 15), start=date(2021, 5, 14))
  460. []
  461. Note that the events can only be found for a date range.
  462. Asking for the events with an out of range date will result in an exception:
  463. >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION]
  464. >>> search_events(event_types, end=date(1000, 1, 2), start=date(1000, 1, 1))
  465. Traceback (most recent call last):
  466. ...
  467. kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-28 and 2053-10-08
  468. If the start date does not occur before the end date, an exception will be thrown
  469. >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION]
  470. >>> search_events(event_types, end=date(2021, 1, 26), start=date(2021, 1, 28))
  471. Traceback (most recent call last):
  472. ...
  473. kosmorrolib.exceptions.InvalidDateRangeError: The start date (2021-01-28) must be before the end date (2021-01-26)
  474. If the start and end dates are the same, then events for that one day will be returned.
  475. >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION]
  476. >>> search_events(event_types, end=date(2021, 1, 28), start=date(2021, 1, 28))
  477. [<Event type=CONJUNCTION objects=[<Object type=PLANET name=VENUS />, <Object type=PLANET name=PLUTO />] start=2021-01-28 16:18:05.483029+00:00 end=None details=None />]
  478. """
  479. moon = ASTERS[1]
  480. sun = ASTERS[0]
  481. def _search_all_apogee_events(
  482. start: date, end: date, utc_offset: Union[int, float] = 0
  483. ) -> [Event]:
  484. moon_apogee_events = _search_apogee(moon)(start, end, utc_offset)
  485. earth_apogee_events = _search_apogee(EARTH, from_aster=sun)(
  486. start, end, utc_offset
  487. )
  488. return moon_apogee_events + earth_apogee_events
  489. def _search_all_perigee_events(
  490. start: date, end: date, utc_offset: Union[int, float] = 0
  491. ) -> [Event]:
  492. moon_perigee_events = _search_perigee(moon)(start, end, utc_offset)
  493. earth_perigee_events = _search_perigee(EARTH, from_aster=sun)(
  494. start, end, utc_offset
  495. )
  496. return moon_perigee_events + earth_perigee_events
  497. search_funcs = {
  498. EventType.OPPOSITION: _search_oppositions,
  499. EventType.CONJUNCTION: _search_conjunctions_occultations,
  500. EventType.OCCULTATION: _search_conjunctions_occultations,
  501. EventType.MAXIMAL_ELONGATION: _search_maximal_elongations,
  502. EventType.APOGEE: _search_all_apogee_events,
  503. EventType.PERIGEE: _search_all_perigee_events,
  504. EventType.SEASON_CHANGE: _search_earth_season_change,
  505. EventType.LUNAR_ECLIPSE: _search_lunar_eclipse,
  506. }
  507. if start > end:
  508. raise InvalidDateRangeError(start, end)
  509. start_time = get_timescale().utc(start.year, start.month, start.day, -utc_offset)
  510. end_time = get_timescale().utc(end.year, end.month, end.day + 1, -utc_offset)
  511. try:
  512. found_events = []
  513. for event_type in event_types:
  514. fun = search_funcs[event_type]
  515. events = fun(start_time, end_time, utc_offset)
  516. for event in events:
  517. if event not in found_events:
  518. found_events.append(event)
  519. return sorted(flatten_list(found_events), key=lambda event: event.start_time)
  520. except EphemerisRangeError as error:
  521. start_date = translate_to_utc_offset(
  522. error.start_time.utc_datetime(), utc_offset
  523. )
  524. end_date = translate_to_utc_offset(error.end_time.utc_datetime(), utc_offset)
  525. start_date = date(start_date.year, start_date.month, start_date.day)
  526. end_date = date(end_date.year, end_date.month, end_date.day)
  527. raise OutOfRangeDateError(start_date, end_date) from error