@@ -19,4 +19,4 @@ jobs: | |||
pipenv sync -d | |||
- name: Lint | |||
run: | | |||
pipenv run pylint *.py | |||
pipenv run pylint *.py kosmorrolib/*.py |
@@ -30,7 +30,7 @@ limit-inference-results=100 | |||
# usually to register additional checkers. | |||
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. | |||
persistent=yes | |||
@@ -144,7 +144,8 @@ disable=print-statement, | |||
missing-docstring, | |||
too-many-locals, | |||
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 | |||
# 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 | |||
$ 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 | |||
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 | |||
# Kosmorro - Compute The Next Ephemeris | |||
# 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 | |||
@@ -19,8 +19,8 @@ | |||
import argparse | |||
from datetime import date | |||
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" | |||
@@ -35,35 +35,42 @@ def main(): | |||
args = get_args() | |||
year = args.year | |||
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: | |||
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) | |||
print(dump.to_string()) | |||
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() | |||
@@ -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 | |||
# Kosmorro - Compute The Next Ephemeris | |||
# 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 | |||
@@ -17,13 +17,16 @@ | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
from abc import ABC, abstractmethod | |||
import datetime | |||
from tabulate import tabulate | |||
from skyfield import almanac | |||
from .data import Object | |||
class Dumper(ABC): | |||
def __init__(self, ephemeris): | |||
def __init__(self, ephemeris, date: datetime.date = datetime.date.today()): | |||
self.ephemeris = ephemeris | |||
self.date = date | |||
@abstractmethod | |||
def to_string(self): | |||
@@ -32,23 +35,36 @@ class Dumper(ABC): | |||
class TextDumper(Dumper): | |||
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 | |||
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]) | |||
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',)) | |||
@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) |