@@ -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 | Planet Rise time Culmination time Set time | ||||
-------- ----------- -------------- ---------- | -------- ----------- ------------------ ---------- | ||||
SUN 06:35 - 16:32 | Sun 06:35 11:33 16:32 | ||||
MERCURY 08:44 13:01 16:59 | Moon 10:21 14:41 19:01 | ||||
VENUS 08:35 13:01 17:18 | Mercury 08:37 12:51 17:04 | ||||
MARS 04:48 10:20 15:51 | Venus 08:28 12:56 17:23 | ||||
JUPITER 10:40 15:01 18:46 | Mars 04:42 10:19 15:55 | ||||
SATURN 12:12 16:20 20:26 | Jupiter 10:33 14:42 18:52 | ||||
URANUS 16:23 - 06:22 | Saturn 12:05 16:18 20:31 | ||||
NEPTUNE 14:53 20:23 01:56 | Uranus 16:17 - 06:27 | ||||
PLUTO 12:36 17:01 20:50 | 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 kosmorrolib import dumper | ||||
from ephemeris import Ephemeris | 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 | day = args.day | ||||
position = {'lat': args.latitude, 'lon': args.longitude, 'alt': args.altitude} | |||||
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) | ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude, altitude=args.altitude)) | ||||
ephemerides = ephemeris.compute_ephemeris(year, month, day) | 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.', | today = date.today() | ||||
epilog='By default, the observer will be set at position (0,0) with an altitude' | parser = argparse.ArgumentParser(description='Compute the ephemerides for a given date, at a given position' | ||||
' of 0. You will more likely want to change that.') | ' on Earth.', | ||||
parser.add_argument('--latitude', '-lat', type=float, default=0., help="The observer's position on Earth" | epilog='By default, the ephemerides will be computed for today (%s) for an' | ||||
" (latitude)") | ' observer positioned at coordinates (0,0), with an altitude of 0.' | ||||
parser.add_argument('--longitude', '-lon', type=float, default=0., help="The observer's position on Earth" | % today.strftime('%a %b %d, %Y')) | ||||
" (longitude)") | parser.add_argument('--latitude', '-lat', type=float, default=0., | ||||
parser.add_argument('--altitude', '-alt', type=float, default=0., help="The observer's position on Earth" | help="The observer's latitude on Earth") | ||||
" (altitude)") | parser.add_argument('--longitude', '-lon', type=float, default=0., | ||||
parser.add_argument('--date', '-d', type=int, help='A number between 1 and 28, 29, 30 or 31 (depending on the' | help="The observer's longitude on Earth") | ||||
' month). The date you want to compute the ephemeris for') | parser.add_argument('--altitude', '-alt', type=float, default=0., | ||||
parser.add_argument('--month', '-m', type=int, help='A number between 1 and 12. The month you want to compute the' | help="The observer's altitude on Earth") | ||||
' ephemeris for (defaults to the current month if the day is' | parser.add_argument('--day', '-d', type=int, default=today.day, | ||||
' defined)') | help='A number between 1 and 28, 29, 30 or 31 (depending on the month). The day you want to ' | ||||
parser.add_argument('year', type=int, help='The year you want to compute the ephemeris for') | ' 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']), | return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'), | ||||
self.get_moon(self.ephemeris['moon'])]) | 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): | def get_asters(asters: [Object]) -> str: | ||||
data = [['SUN', sun['rise'].utc_strftime('%H:%M'), '-', sun['set'].utc_strftime('%H:%M')]] | data = [] | ||||
for planet in planets: | for aster in asters: | ||||
name = planet | name = aster.name | ||||
planet_data = planets[planet] | if aster.ephemerides.rise_time is not None: | ||||
planet_rise = planet_data['rise'].utc_strftime('%H:%M') if planet_data['rise'] is not None else ' -' | planet_rise = aster.ephemerides.rise_time.utc_strftime('%H:%M') | ||||
planet_maximum = planet_data['maximum'].utc_strftime('%H:%M') if planet_data['maximum'] is not None\ | else: | ||||
else ' -' | planet_rise = '-' | ||||
planet_set = planet_data['set'].utc_strftime('%H:%M') if planet_data['set'] is not None else ' -' | 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) |