Browse Source

Merge branch 'refacto'

tags/v0.1.0
Jérôme Deuchnord 5 years ago
parent
commit
3ad2892c03
No known key found for this signature in database GPG Key ID: BC6F3C345B7D33B0
11 changed files with 445 additions and 333 deletions
  1. +1
    -1
      .github/workflows/pythonapp.yml
  2. +3
    -2
      .pylintrc
  3. +34
    -30
      README.md
  4. +0
    -54
      dumper.py
  5. +0
    -224
      ephemeris.py
  6. +31
    -22
      kosmorro.py
  7. +17
    -0
      kosmorrolib/__init__.py
  8. +46
    -0
      kosmorrolib/core.py
  9. +92
    -0
      kosmorrolib/data.py
  10. +72
    -0
      kosmorrolib/dumper.py
  11. +149
    -0
      kosmorrolib/ephemerides.py

+ 1
- 1
.github/workflows/pythonapp.yml View File

@@ -19,4 +19,4 @@ jobs:
pipenv sync -d
- name: Lint
run: |
pipenv run pylint *.py
pipenv run pylint *.py kosmorrolib/*.py

+ 3
- 2
.pylintrc View File

@@ -30,7 +30,7 @@ limit-inference-results=100
# usually to register additional checkers.
load-plugins=pylintfileheader

file-header=# 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/>.
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


+ 34
- 30
README.md View File

@@ -25,49 +25,53 @@ A `setup.py` file will come later to manage installation in the real world.

```
kosmorro.py [-h] [--latitude LATITUDE] [--longitude LONGITUDE]
[--altitude ALTITUDE] [--date DATE] [--month MONTH]
year
[--altitude ALTITUDE] [--day DAY] [--month MONTH]
[--year YEAR]

Compute the ephemeris for a given day/month/year.

positional arguments:
year The year you want to compute the ephemeris for
Compute the ephemerides for a given date, at a given position on Earth.

optional arguments:
-h, --help show this help message and exit
--latitude LATITUDE, -lat LATITUDE
The observer's position on Earth (latitude)
The observer's latitude on Earth
--longitude LONGITUDE, -lon LONGITUDE
The observer's position on Earth (longitude)
The observer's longitude on Earth
--altitude ALTITUDE, -alt ALTITUDE
The observer's position on Earth (altitude)
--date DATE, -d DATE A number between 1 and 28, 29, 30 or 31 (depending on
the month). The date you want to compute the ephemeris
for
The observer's altitude on Earth
--day DAY, -d DAY 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 10 (the current day).
--month MONTH, -m MONTH
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)
compute the ephemerides for. Defaults to 11 (the
current month).
--year YEAR, -y YEAR The year you want to compute the ephemerides for.
Defaults to 2019 (the current year).

By default, the observer will be set at position (0,0) with an altitude of 0.
You will more likely want to change that.
By default, the ephemerides will be computed for today (Sun Nov 10, 2019) for
an observer positioned at coordinates (0,0), with an altitude of 0.
```

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

Moon phase: New Moon
$ python kosmorro.py --latitude 48.8032 --longitude 2.3511 -d 11 -m 11 -y 2019
Ephemerides of Sunday November 10, 2019

Planet Rise time Culmination time Set time
-------- ----------- ------------------ ----------
Sun 06:52 11:34 06:52
Moon 16:12 - 05:17
Mercury 06:57 11:36 06:57
Venus 09:00 13:10 09:00
Mars 04:38 10:02 04:38
Jupiter 10:00 14:09 10:00
Saturn 11:25 15:38 11:25
Uranus 15:33 22:35 05:41
Neptune 14:03 19:38 01:16
Pluto 11:46 15:59 11:46

Moon phase: First Quarter

Note: All the hours are given in UTC.
```

+ 0
- 54
dumper.py View File

@@ -1,54 +0,0 @@
# 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 abc import ABC, abstractmethod
from tabulate import tabulate
from skyfield import almanac


class Dumper(ABC):
def __init__(self, ephemeris):
self.ephemeris = ephemeris

@abstractmethod
def to_string(self):
pass


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'])])

@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 ' -'

data.append([name, planet_rise, planet_maximum, planet_set])

return tabulate(data, headers=['Planet', 'Rise time', 'Maximum time', 'Set time'], tablefmt='simple',
stralign='center', colalign=('left',))

@staticmethod
def get_moon(moon):
return 'Moon phase: %s' % almanac.MOON_PHASES[moon['phase']]

+ 0
- 224
ephemeris.py View File

@@ -1,224 +0,0 @@
# 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 compute_ephemeris_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 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(4) 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_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_ephemeris_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_ephemeris_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)

+ 31
- 22
kosmorro.py View File

@@ -1,4 +1,6 @@
# Kosmorro - Compute The Next Ephemeris
#!/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
@@ -17,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"
@@ -33,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()



+ 17
- 0
kosmorrolib/__init__.py View File

@@ -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/>.

+ 46
- 0
kosmorrolib/core.py View File

@@ -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')

+ 92
- 0
kosmorrolib/data.py View File

@@ -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'

+ 72
- 0
kosmorrolib/dumper.py View File

@@ -0,0 +1,72 @@
#!/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
import datetime
from tabulate import tabulate
from skyfield import almanac
from .data import Object


class Dumper(ABC):
def __init__(self, ephemeris, date: datetime.date = datetime.date.today()):
self.ephemeris = ephemeris
self.date = date

@abstractmethod
def to_string(self):
pass


class TextDumper(Dumper):
def to_string(self):
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_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', 'Culmination time', 'Set time'], tablefmt='simple',
stralign='center', colalign=('left',))

@staticmethod
def get_moon(moon):
return 'Moon phase: %s' % almanac.MOON_PHASES[moon['phase']]

+ 149
- 0
kosmorrolib/ephemerides.py View File

@@ -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)

Loading…
Cancel
Save