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.

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