Skip to content
Permalink
Browse files

feat(events): add support for opposition events

  • Loading branch information
Deuchnord committed Dec 2, 2019
1 parent 0786a42 commit fa2da9e4a9468b4f8ad0fa2b2184fa93c5513fbe
Showing with 204 additions and 13 deletions.
  1. +2 −1 .pylintrc
  2. +6 −1 kosmorro
  3. +21 −0 kosmorrolib/data.py
  4. +31 −5 kosmorrolib/dumper.py
  5. +61 −0 kosmorrolib/events.py
  6. +1 −0 test/__init__.py
  7. +49 −6 test/dumper.py
  8. +33 −0 test/events.py
@@ -145,7 +145,8 @@ disable=print-statement,
too-many-locals,
too-many-branches,
too-few-public-methods,
protected-access
protected-access,
unnecessary-comprehension

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@@ -24,6 +24,7 @@ from kosmorrolib.version import VERSION
from kosmorrolib import dumper
from kosmorrolib import core
from kosmorrolib.ephemerides import EphemeridesComputer, Position
from kosmorrolib import events


def main():
@@ -37,13 +38,17 @@ def main():
month = args.month
day = args.day

compute_date = date(year, month, day)

if day is not None and month is None:
month = date.today().month

ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude))
ephemerides = ephemeris.compute_ephemerides(year, month, day)

dump = output_formats[args.format](ephemerides, date(year, month, day))
events_list = events.search_events(compute_date)

dump = output_formats[args.format](ephemerides, events_list, compute_date)
print(dump.to_string())

return 0
@@ -33,6 +33,10 @@
'WANING_CRESCENT': 'Waning crescent'
}

EVENTS = {
'OPPOSITION': {'message': '%s is in opposition'}
}


class MoonPhase:
def __init__(self, identifier: str, time: Union[Time, None], next_phase_date: Union[Time, None]):
@@ -131,3 +135,20 @@ def get_type(self) -> str:
class Satellite(Object):
def get_type(self) -> str:
return 'satellite'


class Event:
def __init__(self, event_type: str, aster: [Object], start_time: Time, end_time: Union[Time, None] = None):
if event_type not in EVENTS.keys():
raise ValueError('event_type parameter must be one of the following: %s (got %s)' % (
', '.join(EVENTS.keys()),
event_type)
)

self.event_type = event_type
self.object = aster
self.start_time = start_time
self.end_time = end_time

def get_description(self) -> str:
return EVENTS[self.event_type]['message'] % self.object.name
@@ -22,12 +22,13 @@
from tabulate import tabulate
from skyfield.timelib import Time
from numpy import int64
from .data import Object, AsterEphemerides, MoonPhase
from .data import Object, AsterEphemerides, MoonPhase, Event


class Dumper(ABC):
def __init__(self, ephemeris: dict, date: datetime.date = datetime.date.today()):
def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today()):
self.ephemeris = ephemeris
self.events = events
self.date = date

@abstractmethod
@@ -37,6 +38,7 @@ def to_string(self):

class JsonDumper(Dumper):
def to_string(self):
self.ephemeris['events'] = self.events
return json.dumps(self.ephemeris,
default=self._json_default,
indent=4)
@@ -60,16 +62,31 @@ def _json_default(obj):
moon_phase['phase'] = moon_phase.pop('identifier')
moon_phase['date'] = moon_phase.pop('time')
return moon_phase
if isinstance(obj, Event):
event = obj.__dict__
event['object'] = event['object'].name
return event

raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj)))


class TextDumper(Dumper):
def to_string(self):
return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'),
text = 'Ephemerides of %s' % self.date.strftime('%A %B %d, %Y')
text = '\n\n'.join([text,
self.get_asters(self.ephemeris['details']),
self.get_moon(self.ephemeris['moon_phase']),
'Note: All the hours are given in UTC.'])
self.get_moon(self.ephemeris['moon_phase'])
])

if len(self.events) > 0:
text = '\n\n'.join([text,
'Expected events:',
self.get_events(self.events)
])

text = '\n\n'.join([text, 'Note: All the hours are given in UTC.'])

return text

@staticmethod
def get_asters(asters: [Object]) -> str:
@@ -98,6 +115,15 @@ def get_asters(asters: [Object]) -> str:
return tabulate(data, headers=['Object', 'Rise time', 'Culmination time', 'Set time'], tablefmt='simple',
stralign='center', colalign=('left',))

@staticmethod
def get_events(events: [Event]) -> str:
data = []

for event in events:
data.append([event.start_time.utc_strftime('%H:%M'), event.get_description()])

return tabulate(data, tablefmt='plain', stralign='left')

@staticmethod
def get_moon(moon_phase: MoonPhase) -> str:
return 'Moon phase: %s\n' \
@@ -0,0 +1,61 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from datetime import date as date_type

from skyfield.timelib import Time
from skyfield.almanac import find_discrete

from .data import Event, Planet
from .core import get_timescale, get_skf_objects, ASTERS


def _search_oppositions(start_time: Time, end_time: Time) -> [Event]:
earth = get_skf_objects()['earth']
sun = get_skf_objects()['sun']
aster = None

def is_oppositing(time: Time) -> [bool]:
earth_pos = earth.at(time)
sun_pos = earth_pos.observe(sun).apparent() # Never do this without eyes protection!
aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent()
_, lon1, _ = sun_pos.ecliptic_latlon()
_, lon2, _ = aster_pos.ecliptic_latlon()
return (lon1.degrees - lon2.degrees) > 180

is_oppositing.rough_period = 1.0
events = []

for aster in ASTERS:
if not isinstance(aster, Planet) or aster.name in ['Mercury', 'Venus']:
continue

times, _ = find_discrete(start_time, end_time, is_oppositing)
for time in times:
events.append(Event('OPPOSITION', aster, time))

return events


def search_events(date: date_type) -> [Event]:
start_time = get_timescale().utc(date.year, date.month, date.day)
end_time = get_timescale().utc(date.year, date.month, date.day + 1)

return [
opposition for opposition in _search_oppositions(start_time, end_time)
]
@@ -1,2 +1,3 @@
from .dumper import *
from .ephemerides import *
from .events import *
@@ -1,17 +1,22 @@
import unittest
from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase
from kosmorrolib.dumper import JsonDumper
from datetime import date

from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase, Event
from kosmorrolib.dumper import JsonDumper, TextDumper
from kosmorrolib.core import get_timescale


class DumperTestCase(unittest.TestCase):
def setUp(self) -> None:
self.maxDiff = None

def test_json_dumper_returns_correct_json(self):
data = self._get_data()
self.assertEqual('{\n'
' "moon_phase": {\n'
' "next_phase_date": "2019-11-20T00:00:00Z",\n'
' "next_phase_date": "2019-10-21T00:00:00Z",\n'
' "phase": "FULL_MOON",\n'
' "date": "2019-11-11T00:00:00Z"\n'
' "date": "2019-10-14T00:00:00Z"\n'
' },\n'
' "details": [\n'
' {\n'
@@ -22,13 +27,51 @@ def test_json_dumper_returns_correct_json(self):
' "set_time": null\n'
' }\n'
' }\n'
' ],\n'
' "events": [\n'
' {\n'
' "event_type": "OPPOSITION",\n'
' "object": "Mars",\n'
' "start_time": "2018-07-27T05:12:00Z",\n'
' "end_time": null\n'
' }\n'
' ]\n'
'}', JsonDumper(data).to_string())
'}', JsonDumper(data,
[Event('OPPOSITION', Planet('Mars', 'MARS'),
get_timescale().utc(2018, 7, 27, 5, 12))]
).to_string())

def test_text_dumper_without_events(self):
ephemerides = self._get_data()
self.assertEqual('Ephemerides of Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n'
'Mars - - -\n\n'
'Moon phase: Full Moon\n'
'Last Quarter on Mon Oct 21, 2019 00:00\n\n'
'Note: All the hours are given in UTC.',
TextDumper(ephemerides, [], date=date(2019, 10, 14)).to_string())

def test_text_dumper_with_events(self):
ephemerides = self._get_data()
self.assertEqual('Ephemerides of Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n'
'Mars - - -\n\n'
'Moon phase: Full Moon\n'
'Last Quarter on Mon Oct 21, 2019 00:00\n\n'
'Expected events:\n\n'
'05:12 Mars is in opposition\n\n'
'Note: All the hours are given in UTC.',
TextDumper(ephemerides, [Event('OPPOSITION',
Planet('Mars', 'MARS'),
get_timescale().utc(2018, 7, 27, 5, 12))
], date=date(2019, 10, 14)).to_string())

@staticmethod
def _get_data():
return {
'moon_phase': MoonPhase('FULL_MOON', get_timescale().utc(2019, 11, 11), get_timescale().utc(2019, 11, 20)),
'moon_phase': MoonPhase('FULL_MOON', get_timescale().utc(2019, 10, 14), get_timescale().utc(2019, 10, 21)),
'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))]
}

@@ -0,0 +1,33 @@
import unittest

from datetime import date

from kosmorrolib import events
from kosmorrolib.data import Event
from kosmorrolib.core import get_timescale


class MyTestCase(unittest.TestCase):
def test_event_only_accepts_valid_values(self):
with self.assertRaises(ValueError):
Event('SUPERNOVA', None, get_timescale().now())

def test_find_oppositions(self):
# Test case: Mars opposition
# Source of the information: https://promenade.imcce.fr/en/pages6/887.html#mar
o1 = (events.search_events(date(2020, 10, 13)), '^2020-10-13T23:25')
o2 = (events.search_events(date(2022, 12, 8)), '^2022-12-08T05:41')
o3 = (events.search_events(date(2025, 1, 16)), '^2025-01-16T02:38')
o4 = (events.search_events(date(2027, 2, 19)), '^2027-02-19T15:50')

for (o, expected_date) in [o1, o2, o3, o4]:
self.assertEqual(1, len(o), 'Expected 1 event for %s, got %d' % (expected_date, len(o)))
self.assertEqual('OPPOSITION', o[0].event_type)
self.assertEqual('MARS', o[0].object.skyfield_name)
self.assertRegex(o[0].start_time.utc_iso(), expected_date)
self.assertIsNone(o[0].end_time)
self.assertEqual('Mars is in opposition', o[0].get_description())


if __name__ == '__main__':
unittest.main()

0 comments on commit fa2da9e

Please sign in to comment.
You can’t perform that action at this time.