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.

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