| @@ -19,4 +19,4 @@ jobs: | |||||
| pipenv sync -d | pipenv sync -d | ||||
| - name: Lint | - name: Lint | ||||
| run: | | run: | | ||||
| pipenv run pylint *.py | |||||
| pipenv run pylint *.py kosmorrolib/*.py | |||||
| @@ -30,7 +30,7 @@ limit-inference-results=100 | |||||
| # usually to register additional checkers. | # usually to register additional checkers. | ||||
| load-plugins=pylintfileheader | load-plugins=pylintfileheader | ||||
| file-header=#!/usr/bin/env python3\n\n# Kosmorro - Compute The Next Ephemeris\n# Copyright \(C\) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as\n# published by the Free Software Foundation, either version 3 of the\n# License, or \(at your option\) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program. If not, see <https://www.gnu.org/licenses/>.\n\n | |||||
| file-header=#!/usr/bin/env python3\n\n# Kosmorro - Compute The Next Ephemerides\n# Copyright \(C\) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as\n# published by the Free Software Foundation, either version 3 of the\n# License, or \(at your option\) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program. If not, see <https://www.gnu.org/licenses/>.\n | |||||
| # Pickle collected data for later comparisons. | # Pickle collected data for later comparisons. | ||||
| persistent=yes | persistent=yes | ||||
| @@ -144,7 +144,8 @@ disable=print-statement, | |||||
| missing-docstring, | missing-docstring, | ||||
| too-many-locals, | too-many-locals, | ||||
| too-many-branches, | too-many-branches, | ||||
| too-few-public-methods | |||||
| too-few-public-methods, | |||||
| protected-access | |||||
| # Enable the message, report, category or checker with the given id(s). You can | # 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 | # either give multiple identifier separated by comma (,) or put this option | ||||
| @@ -57,17 +57,20 @@ For instance, if you want the ephemeris of October 31th, 2019 in Paris, France: | |||||
| ```console | ```console | ||||
| $ python kosmorro.py --latitude 48.8032 --longitude 2.3511 -m 10 -d 31 2019 | $ python kosmorro.py --latitude 48.8032 --longitude 2.3511 -m 10 -d 31 2019 | ||||
| Planet Rise time Maximum time Set time | |||||
| -------- ----------- -------------- ---------- | |||||
| SUN 06:35 - 16:32 | |||||
| MERCURY 08:44 13:01 16:59 | |||||
| VENUS 08:35 13:01 17:18 | |||||
| MARS 04:48 10:20 15:51 | |||||
| JUPITER 10:40 15:01 18:46 | |||||
| SATURN 12:12 16:20 20:26 | |||||
| URANUS 16:23 - 06:22 | |||||
| NEPTUNE 14:53 20:23 01:56 | |||||
| PLUTO 12:36 17:01 20:50 | |||||
| Planet Rise time Culmination time Set time | |||||
| -------- ----------- ------------------ ---------- | |||||
| Sun 06:35 11:33 16:32 | |||||
| Moon 10:21 14:41 19:01 | |||||
| Mercury 08:37 12:51 17:04 | |||||
| Venus 08:28 12:56 17:23 | |||||
| Mars 04:42 10:19 15:55 | |||||
| Jupiter 10:33 14:42 18:52 | |||||
| Saturn 12:05 16:18 20:31 | |||||
| Uranus 16:17 - 06:27 | |||||
| Neptune 14:47 20:21 02:00 | |||||
| Pluto 12:29 16:42 20:55 | |||||
| Moon phase: New Moon | Moon phase: New Moon | ||||
| Note: All the hours are given in UTC. | |||||
| ``` | ``` | ||||
| @@ -1,226 +0,0 @@ | |||||
| #!/usr/bin/env python3 | |||||
| # Kosmorro - Compute The Next Ephemeris | |||||
| # 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 multiprocessing import Pool as ThreadPool | |||||
| from skyfield.api import Loader, Topos | |||||
| from skyfield import almanac | |||||
| class Ephemeris: | |||||
| MONTH = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] | |||||
| PLANETS = ['MERCURY', 'VENUS', 'MARS', 'JUPITER BARYCENTER', 'SATURN BARYCENTER', 'URANUS BARYCENTER', | |||||
| 'NEPTUNE BARYCENTER', 'PLUTO BARYCENTER'] | |||||
| position = None | |||||
| latitude = None | |||||
| longitude = None | |||||
| timescale = None | |||||
| planets = None | |||||
| def __init__(self, position): | |||||
| load = Loader('./cache') | |||||
| self.timescale = load.timescale() | |||||
| self.planets = load('de421.bsp') | |||||
| self.latitude = position['lat'] | |||||
| self.longitude = position['lon'] | |||||
| self.position = Topos(latitude_degrees=position['lat'], longitude_degrees=position['lon']) | |||||
| def get_sun(self, start_time, end_time) -> dict: | |||||
| times, is_risen = almanac.find_discrete(start_time, | |||||
| end_time, | |||||
| almanac.sunrise_sunset(self.planets, self.position)) | |||||
| sunrise = times[0] if is_risen[0] else times[1] | |||||
| sunset = times[1] if not is_risen[1] else times[0] | |||||
| return {'rise': sunrise, 'set': sunset} | |||||
| def get_moon(self, year, month, day) -> dict: | |||||
| time1 = self.timescale.utc(year, month, day - 10) | |||||
| time2 = self.timescale.utc(year, month, day) | |||||
| _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(self.planets)) | |||||
| return {'phase': moon_phase[-1]} | |||||
| def get_planets(self, year: int, month: int, day: int) -> dict: | |||||
| args = [] | |||||
| for planet_name in self.PLANETS: | |||||
| args.append({'planet': planet_name, | |||||
| 'observer': {'latitude': self.latitude, 'longitude': self.longitude}, | |||||
| 'year': year, 'month': month, 'day': day}) | |||||
| with ThreadPool() as pool: | |||||
| planets = pool.map(Ephemeris.get_planet, args) | |||||
| obj = {} | |||||
| for planet in planets: | |||||
| obj[planet['name'].split(' ')[0]] = {'rise': planet['rise'], 'maximum': planet['maximum'], | |||||
| 'set': planet['set']} | |||||
| return obj | |||||
| @staticmethod | |||||
| def get_planet(p_obj) -> dict: | |||||
| load = Loader('./cache') | |||||
| planets = load('de421.bsp') | |||||
| timescale = load.timescale() | |||||
| position = Topos(latitude_degrees=p_obj['observer']['latitude'], | |||||
| longitude_degrees=p_obj['observer']['longitude']) | |||||
| observer = planets['earth'] + position | |||||
| planet = planets[p_obj['planet']] | |||||
| rise_time = maximum_time = set_time = None | |||||
| is_risen = None | |||||
| is_elevating = None | |||||
| last_is_elevating = None | |||||
| last_position = None | |||||
| for hours in range(0, 24): | |||||
| time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours) | |||||
| position = observer.at(time).observe(planet).apparent().altaz()[0].degrees | |||||
| if is_risen is None: | |||||
| is_risen = position > 0 | |||||
| if last_position is not None: | |||||
| is_elevating = last_position < position | |||||
| if rise_time is None and not is_risen and is_elevating and position > 0: | |||||
| # Planet has risen in the last hour, let's look for a more precise time! | |||||
| for minutes in range(0, 60): | |||||
| time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes) | |||||
| position = observer.at(time).observe(planet).apparent().altaz()[0].degrees | |||||
| if position > 0: | |||||
| # Planet has just risen! | |||||
| rise_time = time | |||||
| is_risen = True | |||||
| break | |||||
| if set_time is None and is_risen and not is_elevating and position < 0: | |||||
| # Planet has set in the last hour, let's look for a more precise time! | |||||
| for minutes in range(0, 60): | |||||
| time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes) | |||||
| position = observer.at(time).observe(planet).apparent().altaz()[0].degrees | |||||
| if position < 0: | |||||
| # Planet has just set! | |||||
| set_time = time | |||||
| is_risen = False | |||||
| break | |||||
| if not is_elevating and last_is_elevating: | |||||
| # Planet has reached its azimuth in the last hour, let's look for a more precise time! | |||||
| for minutes in range(0, 60): | |||||
| time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes) | |||||
| position = observer.at(time).observe(planet).apparent().altaz()[0].degrees | |||||
| maximum_time = time | |||||
| if last_position > position: | |||||
| # Planet has reached its azimuth! | |||||
| is_elevating = False | |||||
| break | |||||
| last_position = position | |||||
| last_position = position | |||||
| last_is_elevating = is_elevating | |||||
| if rise_time is not None and set_time is not None and maximum_time is not None: | |||||
| return { | |||||
| 'name': p_obj['planet'], | |||||
| 'rise': rise_time, | |||||
| 'maximum': maximum_time, | |||||
| 'set': set_time | |||||
| } | |||||
| return { | |||||
| 'name': p_obj['planet'], | |||||
| 'rise': rise_time if rise_time is not None else None, | |||||
| 'maximum': maximum_time if maximum_time is not None else None, | |||||
| 'set': set_time if set_time is not None else None | |||||
| } | |||||
| def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: | |||||
| ephemeris = {} | |||||
| time1 = self.timescale.utc(year, month, day, 2) | |||||
| time2 = self.timescale.utc(year, month, day + 1, 2) | |||||
| # Compute sunrise and sunset | |||||
| ephemeris['sun'] = self.get_sun(time1, time2) | |||||
| ephemeris['moon'] = self.get_moon(year, month, day) | |||||
| ephemeris['planets'] = self.get_planets(year, month, day) | |||||
| return ephemeris | |||||
| def compute_ephemerides_for_month(self, year: int, month: int) -> list: | |||||
| if month == 2: | |||||
| is_leap_year = (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) | |||||
| max_day = 29 if is_leap_year else 28 | |||||
| elif month < 8: | |||||
| max_day = 30 if month % 2 == 0 else 31 | |||||
| else: | |||||
| max_day = 31 if month % 2 == 0 else 30 | |||||
| ephemerides = [] | |||||
| for day in range(1, max_day + 1): | |||||
| ephemerides.append(self.compute_ephemerides_for_day(year, month, day)) | |||||
| return ephemerides | |||||
| def compute_ephemerides_for_year(self, year: int) -> dict: | |||||
| ephemerides = {} | |||||
| for month in range(0, 12): | |||||
| ephemerides[self.MONTH[month]] = self.compute_ephemerides_for_month(year, month + 1) | |||||
| ephemerides['seasons'] = self.get_seasons(year) | |||||
| return ephemerides | |||||
| def get_seasons(self, year: int) -> dict: | |||||
| start_time = self.timescale.utc(year, 1, 1) | |||||
| end_time = self.timescale.utc(year, 12, 31) | |||||
| times, almanac_seasons = almanac.find_discrete(start_time, end_time, almanac.seasons(self.planets)) | |||||
| seasons = {} | |||||
| for time, almanac_season in zip(times, almanac_seasons): | |||||
| if almanac_season == 0: | |||||
| season = 'MARCH' | |||||
| elif almanac_season == 1: | |||||
| season = 'JUNE' | |||||
| elif almanac_season == 2: | |||||
| season = 'SEPTEMBER' | |||||
| elif almanac_season == 3: | |||||
| season = 'DECEMBER' | |||||
| else: | |||||
| raise AssertionError | |||||
| seasons[season] = time.utc_iso() | |||||
| return seasons | |||||
| def compute_ephemeris(self, year: int, month: int, day: int): | |||||
| if day is not None: | |||||
| return self.compute_ephemerides_for_day(year, month, day) | |||||
| if month is not None: | |||||
| return self.compute_ephemerides_for_month(year, month) | |||||
| return self.compute_ephemerides_for_year(year) | |||||
| @@ -1,6 +1,6 @@ | |||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
| # Kosmorro - Compute The Next Ephemeris | |||||
| # Kosmorro - Compute The Next Ephemerides | |||||
| # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | ||||
| # | # | ||||
| # This program is free software: you can redistribute it and/or modify | # This program is free software: you can redistribute it and/or modify | ||||
| @@ -19,8 +19,8 @@ | |||||
| import argparse | import argparse | ||||
| from datetime import date | from datetime import date | ||||
| import numpy | import numpy | ||||
| import dumper | |||||
| from ephemeris import Ephemeris | |||||
| from kosmorrolib import dumper | |||||
| from kosmorrolib.ephemerides import EphemeridesComputer, Position | |||||
| # Fixes the "TypeError: Object of type int64 is not JSON serializable" | # Fixes the "TypeError: Object of type int64 is not JSON serializable" | ||||
| @@ -35,35 +35,42 @@ def main(): | |||||
| args = get_args() | args = get_args() | ||||
| year = args.year | year = args.year | ||||
| month = args.month | month = args.month | ||||
| day = args.date | |||||
| position = {'lat': args.latitude, 'lon': args.longitude, 'alt': args.altitude} | |||||
| day = args.day | |||||
| if day is not None and month is None: | if day is not None and month is None: | ||||
| month = date.today().month | month = date.today().month | ||||
| ephemeris = Ephemeris(position) | |||||
| ephemerides = ephemeris.compute_ephemeris(year, month, day) | |||||
| ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude, altitude=args.altitude)) | |||||
| ephemerides = ephemeris.compute_ephemerides(year, month, day) | |||||
| dump = dumper.TextDumper(ephemerides) | dump = dumper.TextDumper(ephemerides) | ||||
| print(dump.to_string()) | print(dump.to_string()) | ||||
| def get_args(): | def get_args(): | ||||
| parser = argparse.ArgumentParser(description='Compute the ephemeris for a given day/month/year.', | |||||
| epilog='By default, the observer will be set at position (0,0) with an altitude' | |||||
| ' of 0. You will more likely want to change that.') | |||||
| parser.add_argument('--latitude', '-lat', type=float, default=0., help="The observer's position on Earth" | |||||
| " (latitude)") | |||||
| parser.add_argument('--longitude', '-lon', type=float, default=0., help="The observer's position on Earth" | |||||
| " (longitude)") | |||||
| parser.add_argument('--altitude', '-alt', type=float, default=0., help="The observer's position on Earth" | |||||
| " (altitude)") | |||||
| parser.add_argument('--date', '-d', type=int, help='A number between 1 and 28, 29, 30 or 31 (depending on the' | |||||
| ' month). The date you want to compute the ephemeris for') | |||||
| parser.add_argument('--month', '-m', type=int, help='A number between 1 and 12. The month you want to compute the' | |||||
| ' ephemeris for (defaults to the current month if the day is' | |||||
| ' defined)') | |||||
| parser.add_argument('year', type=int, help='The year you want to compute the ephemeris for') | |||||
| today = date.today() | |||||
| parser = argparse.ArgumentParser(description='Compute the ephemerides for a given date, at a given position' | |||||
| ' on Earth.', | |||||
| epilog='By default, the ephemerides will be computed for today (%s) for an' | |||||
| ' observer positioned at coordinates (0,0), with an altitude of 0.' | |||||
| % today.strftime('%a %b %d, %Y')) | |||||
| parser.add_argument('--latitude', '-lat', type=float, default=0., | |||||
| help="The observer's latitude on Earth") | |||||
| parser.add_argument('--longitude', '-lon', type=float, default=0., | |||||
| help="The observer's longitude on Earth") | |||||
| parser.add_argument('--altitude', '-alt', type=float, default=0., | |||||
| help="The observer's altitude on Earth") | |||||
| parser.add_argument('--day', '-d', type=int, default=today.day, | |||||
| help='A number between 1 and 28, 29, 30 or 31 (depending on the month). The day you want to ' | |||||
| ' compute the ephemerides for. Defaults to %d (the current day).' % today.day) | |||||
| parser.add_argument('--month', '-m', type=int, default=today.month, | |||||
| help='A number between 1 and 12. The month you want to compute the ephemerides for. Defaults to' | |||||
| ' %d (the current month).' % today.month) | |||||
| parser.add_argument('--year', '-y', type=int, default=today.year, | |||||
| help='The year you want to compute the ephemerides for.' | |||||
| ' Defaults to %d (the current year).' % today.year) | |||||
| return parser.parse_args() | return parser.parse_args() | ||||
| @@ -0,0 +1,17 @@ | |||||
| #!/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/>. | |||||
| @@ -0,0 +1,46 @@ | |||||
| #!/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 skyfield.api import Loader | |||||
| from .data import Star, Planet, Satellite | |||||
| MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] | |||||
| ASTERS = [Star('Sun', 'SUN'), | |||||
| Satellite('Moon', 'MOON'), | |||||
| Planet('Mercury', 'MERCURY'), | |||||
| Planet('Venus', 'VENUS'), | |||||
| Planet('Mars', 'MARS'), | |||||
| Planet('Jupiter', 'JUPITER BARYCENTER'), | |||||
| Planet('Saturn', 'SATURN BARYCENTER'), | |||||
| Planet('Uranus', 'URANUS BARYCENTER'), | |||||
| Planet('Neptune', 'NEPTUNE BARYCENTER'), | |||||
| Planet('Pluto', 'PLUTO BARYCENTER')] | |||||
| def get_loader(): | |||||
| return Loader('./cache') | |||||
| def get_timescale(): | |||||
| return get_loader().timescale() | |||||
| def get_skf_objects(): | |||||
| return get_loader()('de421.bsp') | |||||
| @@ -0,0 +1,92 @@ | |||||
| #!/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 abc import ABC, abstractmethod | |||||
| from typing import Union | |||||
| from skyfield.api import Topos | |||||
| from skyfield.timelib import Time | |||||
| class Position: | |||||
| def __init__(self, latitude: float, longitude: float, altitude: float = 0): | |||||
| self.latitude = latitude | |||||
| self.longitude = longitude | |||||
| self.altitude = altitude | |||||
| self.observation_planet = None | |||||
| def get_planet_topos(self) -> Topos: | |||||
| if self.observation_planet is None: | |||||
| raise TypeError('Observation planet must be set.') | |||||
| return self.observation_planet + Topos(latitude_degrees=self.latitude, longitude_degrees=self.longitude) | |||||
| class AsterEphemerides: | |||||
| def __init__(self, | |||||
| rise_time: Union[Time, None], | |||||
| culmination_time: Union[Time, None], | |||||
| set_time: Union[Time, None]): | |||||
| self.rise_time = rise_time | |||||
| self.maximum_time = culmination_time | |||||
| self.set_time = set_time | |||||
| class Object(ABC): | |||||
| """ | |||||
| An astronomical object. | |||||
| """ | |||||
| def __init__(self, | |||||
| name: str, | |||||
| skyfield_name: str, | |||||
| ephemerides: AsterEphemerides or None = None): | |||||
| """ | |||||
| Initialize an astronomical object | |||||
| :param str name: the official name of the object (may be internationalized) | |||||
| :param str skyfield_name: the internal name of the object in Skyfield library | |||||
| :param AsterEphemerides ephemerides: the ephemerides associated to the object | |||||
| """ | |||||
| self.name = name | |||||
| self.skyfield_name = skyfield_name | |||||
| self.ephemerides = ephemerides | |||||
| @abstractmethod | |||||
| def get_type(self) -> str: | |||||
| pass | |||||
| class Star(Object): | |||||
| def get_type(self) -> str: | |||||
| return 'star' | |||||
| class Planet(Object): | |||||
| def get_type(self) -> str: | |||||
| return 'planet' | |||||
| class DwarfPlanet(Planet): | |||||
| def get_type(self) -> str: | |||||
| return 'dwarf_planet' | |||||
| class Satellite(Object): | |||||
| def get_type(self) -> str: | |||||
| return 'satellite' | |||||
| @@ -1,6 +1,6 @@ | |||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
| # Kosmorro - Compute The Next Ephemeris | |||||
| # Kosmorro - Compute The Next Ephemerides | |||||
| # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | ||||
| # | # | ||||
| # This program is free software: you can redistribute it and/or modify | # This program is free software: you can redistribute it and/or modify | ||||
| @@ -17,13 +17,16 @@ | |||||
| # along with this program. If not, see <https://www.gnu.org/licenses/>. | # along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||
| import datetime | |||||
| from tabulate import tabulate | from tabulate import tabulate | ||||
| from skyfield import almanac | from skyfield import almanac | ||||
| from .data import Object | |||||
| class Dumper(ABC): | class Dumper(ABC): | ||||
| def __init__(self, ephemeris): | |||||
| def __init__(self, ephemeris, date: datetime.date = datetime.date.today()): | |||||
| self.ephemeris = ephemeris | self.ephemeris = ephemeris | ||||
| self.date = date | |||||
| @abstractmethod | @abstractmethod | ||||
| def to_string(self): | def to_string(self): | ||||
| @@ -32,23 +35,36 @@ class Dumper(ABC): | |||||
| class TextDumper(Dumper): | class TextDumper(Dumper): | ||||
| def to_string(self): | def to_string(self): | ||||
| return '\n\n'.join([self.get_planets(self.ephemeris['planets'], self.ephemeris['sun']), | |||||
| self.get_moon(self.ephemeris['moon'])]) | |||||
| return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'), | |||||
| self.get_asters(self.ephemeris['planets']), | |||||
| self.get_moon(self.ephemeris['moon_phase']), | |||||
| 'Note: All the hours are given in UTC.']) | |||||
| @staticmethod | @staticmethod | ||||
| def get_planets(planets, sun): | |||||
| data = [['SUN', sun['rise'].utc_strftime('%H:%M'), '-', sun['set'].utc_strftime('%H:%M')]] | |||||
| for planet in planets: | |||||
| name = planet | |||||
| planet_data = planets[planet] | |||||
| planet_rise = planet_data['rise'].utc_strftime('%H:%M') if planet_data['rise'] is not None else ' -' | |||||
| planet_maximum = planet_data['maximum'].utc_strftime('%H:%M') if planet_data['maximum'] is not None\ | |||||
| else ' -' | |||||
| planet_set = planet_data['set'].utc_strftime('%H:%M') if planet_data['set'] is not None else ' -' | |||||
| def get_asters(asters: [Object]) -> str: | |||||
| data = [] | |||||
| for aster in asters: | |||||
| name = aster.name | |||||
| if aster.ephemerides.rise_time is not None: | |||||
| planet_rise = aster.ephemerides.rise_time.utc_strftime('%H:%M') | |||||
| else: | |||||
| planet_rise = '-' | |||||
| if aster.ephemerides.maximum_time is not None: | |||||
| planet_maximum = aster.ephemerides.maximum_time.utc_strftime('%H:%M') | |||||
| else: | |||||
| planet_maximum = '-' | |||||
| if aster.ephemerides.set_time is not None: | |||||
| planet_set = aster.ephemerides.set_time.utc_strftime('%H:%M') | |||||
| else: | |||||
| planet_set = '-' | |||||
| data.append([name, planet_rise, planet_maximum, planet_set]) | data.append([name, planet_rise, planet_maximum, planet_set]) | ||||
| return tabulate(data, headers=['Planet', 'Rise time', 'Maximum time', 'Set time'], tablefmt='simple', | |||||
| return tabulate(data, headers=['Planet', 'Rise time', 'Culmination time', 'Set time'], tablefmt='simple', | |||||
| stralign='center', colalign=('left',)) | stralign='center', colalign=('left',)) | ||||
| @staticmethod | @staticmethod | ||||
| @@ -0,0 +1,149 @@ | |||||
| #!/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/>. | |||||
| import datetime | |||||
| from skyfield import almanac | |||||
| from skyfield.timelib import Time | |||||
| from .data import Object, Position, AsterEphemerides | |||||
| from .core import get_skf_objects, get_timescale, ASTERS, MONTHS | |||||
| RISEN_ANGLE = -0.8333 | |||||
| class EphemeridesComputer: | |||||
| def __init__(self, position: Position): | |||||
| position.observation_planet = get_skf_objects()['earth'] | |||||
| self.position = position | |||||
| def get_sun(self, start_time, end_time) -> dict: | |||||
| times, is_risen = almanac.find_discrete(start_time, | |||||
| end_time, | |||||
| almanac.sunrise_sunset(get_skf_objects(), self.position)) | |||||
| sunrise = times[0] if is_risen[0] else times[1] | |||||
| sunset = times[1] if not is_risen[1] else times[0] | |||||
| return {'rise': sunrise, 'set': sunset} | |||||
| @staticmethod | |||||
| def get_moon(year, month, day) -> dict: | |||||
| time1 = get_timescale().utc(year, month, day - 10) | |||||
| time2 = get_timescale().utc(year, month, day) | |||||
| _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects())) | |||||
| return {'phase': moon_phase[-1]} | |||||
| @staticmethod | |||||
| def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | |||||
| skyfield_aster = get_skf_objects()[aster.skyfield_name] | |||||
| def get_angle(time: Time) -> float: | |||||
| return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees | |||||
| def is_risen(time: Time) -> bool: | |||||
| return get_angle(time) > RISEN_ANGLE | |||||
| get_angle.rough_period = 1.0 | |||||
| is_risen.rough_period = 0.5 | |||||
| start_time = get_timescale().utc(date.year, date.month, date.day) | |||||
| end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) | |||||
| rise_times, arr = almanac.find_discrete(start_time, end_time, is_risen) | |||||
| try: | |||||
| culmination_time, _ = almanac._find_maxima(start_time, end_time, get_angle, epsilon=1./3600/24) | |||||
| except ValueError: | |||||
| culmination_time = None | |||||
| if len(rise_times) == 2: | |||||
| rise_time = rise_times[0 if arr[0] else 1] | |||||
| set_time = rise_times[0 if not arr[1] else 0] | |||||
| else: | |||||
| rise_time = rise_times[0] if arr[0] else None | |||||
| set_time = rise_times[0] if not arr[0] else None | |||||
| culmination_time = culmination_time[0] if culmination_time is not None else None | |||||
| aster.ephemerides = AsterEphemerides(rise_time, culmination_time, set_time) | |||||
| return aster | |||||
| @staticmethod | |||||
| def is_leap_year(year: int) -> bool: | |||||
| return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) | |||||
| def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: | |||||
| return {'moon_phase': self.get_moon(year, month, day), | |||||
| 'planets': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) | |||||
| for aster in ASTERS]} | |||||
| def compute_ephemerides_for_month(self, year: int, month: int) -> [dict]: | |||||
| if month == 2: | |||||
| max_day = 29 if self.is_leap_year(year) else 28 | |||||
| elif month < 8: | |||||
| max_day = 30 if month % 2 == 0 else 31 | |||||
| else: | |||||
| max_day = 31 if month % 2 == 0 else 30 | |||||
| ephemerides = [] | |||||
| for day in range(1, max_day + 1): | |||||
| ephemerides.append(self.compute_ephemerides_for_day(year, month, day)) | |||||
| return ephemerides | |||||
| def compute_ephemerides_for_year(self, year: int) -> [dict]: | |||||
| ephemerides = {'seasons': self.get_seasons(year)} | |||||
| for month in range(0, 12): | |||||
| ephemerides[MONTHS[month]] = self.compute_ephemerides_for_month(year, month + 1) | |||||
| return ephemerides | |||||
| @staticmethod | |||||
| def get_seasons(year: int) -> dict: | |||||
| start_time = get_timescale().utc(year, 1, 1) | |||||
| end_time = get_timescale().utc(year, 12, 31) | |||||
| times, almanac_seasons = almanac.find_discrete(start_time, end_time, almanac.seasons(get_skf_objects())) | |||||
| seasons = {} | |||||
| for time, almanac_season in zip(times, almanac_seasons): | |||||
| if almanac_season == 0: | |||||
| season = 'MARCH' | |||||
| elif almanac_season == 1: | |||||
| season = 'JUNE' | |||||
| elif almanac_season == 2: | |||||
| season = 'SEPTEMBER' | |||||
| elif almanac_season == 3: | |||||
| season = 'DECEMBER' | |||||
| else: | |||||
| raise AssertionError | |||||
| seasons[season] = time.utc_iso() | |||||
| return seasons | |||||
| def compute_ephemerides(self, year: int, month: int, day: int): | |||||
| if day is not None: | |||||
| return self.compute_ephemerides_for_day(year, month, day) | |||||
| if month is not None: | |||||
| return self.compute_ephemerides_for_month(year, month) | |||||
| return self.compute_ephemerides_for_year(year) | |||||