diff --git a/kosmorrolib/enum.py b/kosmorrolib/enum.py index ea9d392..fb184d2 100644 --- a/kosmorrolib/enum.py +++ b/kosmorrolib/enum.py @@ -49,6 +49,15 @@ class EventType(Enum): MOON_PERIGEE = 5 MOON_APOGEE = 6 SEASON_CHANGE = 7 + LUNAR_ECLIPSE = 8 + + +class LunarEclipseType(Enum): + """An enumeration of lunar eclipse types""" + + PENUMBRAL = 0 + PARTIAL = 1 + TOTAL = 2 class ObjectType(Enum): diff --git a/kosmorrolib/events.py b/kosmorrolib/events.py index a1eca03..1313a74 100644 --- a/kosmorrolib/events.py +++ b/kosmorrolib/events.py @@ -16,17 +16,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from datetime import date +from datetime import date, timedelta from skyfield.errors import EphemerisRangeError from skyfield.timelib import Time from skyfield.searchlib import find_discrete, find_maxima, find_minima -from skyfield import almanac +from skyfield.units import Angle +from skyfield import almanac, eclipselib from numpy import pi -from kosmorrolib.model import Event, Star, Planet, ASTERS +from kosmorrolib.model import Event, Star, Planet, ASTERS, EARTH from kosmorrolib.dateutil import translate_to_timezone -from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType +from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType, LunarEclipseType from kosmorrolib.exceptions import OutOfRangeDateError from kosmorrolib.core import get_timescale, get_skf_objects, flatten_list @@ -318,6 +319,82 @@ def _search_earth_season_change( return events +def _search_lunar_eclipse(start_time: Time, end_time: Time, timezone: int) -> [Event]: + """Function to detect lunar eclipses. + + **Warning:** this is an internal function, not intended for use by end-developers. + + Will return a total lunar eclipse for 2021-05-26: + + >>> _search_lunar_eclipse(get_timescale().utc(2021, 5, 26), get_timescale().utc(2021, 5, 27), 0) + [] start=2021-05-26 08:47:54.795821+00:00 end=2021-05-26 13:49:34.353411+00:00 details={'type': , 'maximum': datetime.datetime(2021, 5, 26, 11, 18, 42, 328842, tzinfo=datetime.timezone.utc)} />] + + >>> _search_lunar_eclipse(get_timescale().utc(2019, 7, 16), get_timescale().utc(2019, 7, 17), 0) + [] start=2019-07-16 18:39:53.391337+00:00 end=2019-07-17 00:21:51.378940+00:00 details={'type': , 'maximum': datetime.datetime(2019, 7, 16, 21, 30, 44, 170096, tzinfo=datetime.timezone.utc)} />] + + >>> _search_lunar_eclipse(get_timescale().utc(2017, 2, 11), get_timescale().utc(2017, 2, 12), 0) + [] start=2017-02-10 22:02:59.016572+00:00 end=2017-02-11 03:25:07.627886+00:00 details={'type': , 'maximum': datetime.datetime(2017, 2, 11, 0, 43, 51, 793786, tzinfo=datetime.timezone.utc)} />] + """ + moon = ASTERS[1] + events = [] + t, y, details = eclipselib.lunar_eclipses(start_time, end_time, get_skf_objects()) + + for ti, yi in zip(t, y): + penumbra_radius = Angle(radians=details["penumbra_radius_radians"][0]) + _, max_lon, _ = ( + EARTH.get_skyfield_object() + .at(ti) + .observe(moon.get_skyfield_object()) + .apparent() + .ecliptic_latlon() + ) + + def is_in_penumbra(time: Time): + _, lon, _ = ( + EARTH.get_skyfield_object() + .at(time) + .observe(moon.get_skyfield_object()) + .apparent() + .ecliptic_latlon() + ) + + moon_radius = details["moon_radius_radians"] + + return ( + abs(max_lon.radians - lon.radians) + < penumbra_radius.radians + moon_radius + ) + + is_in_penumbra.rough_period = 60.0 + + search_start_time = get_timescale().from_datetime( + start_time.utc_datetime() - timedelta(days=1) + ) + search_end_time = get_timescale().from_datetime( + end_time.utc_datetime() + timedelta(days=1) + ) + + eclipse_start, _ = find_discrete(search_start_time, ti, is_in_penumbra) + eclipse_end, _ = find_discrete(ti, search_end_time, is_in_penumbra) + + events.append( + Event( + EventType.LUNAR_ECLIPSE, + [moon], + start_time=translate_to_timezone( + eclipse_start[0].utc_datetime(), timezone + ), + end_time=translate_to_timezone(eclipse_end[0].utc_datetime(), timezone), + details={ + "type": LunarEclipseType(yi), + "maximum": translate_to_timezone(ti.utc_datetime(), timezone), + }, + ) + ) + + return events + + def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: """Calculate and return a list of events for the given date, adjusted to the given timezone if any. @@ -363,6 +440,7 @@ def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: _search_moon_apogee, _search_moon_perigee, _search_earth_season_change, + _search_lunar_eclipse, ]: found_events.append(fun(start_time, end_time, timezone))