diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b49a22a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/cache \ No newline at end of file diff --git a/Pipfile b/Pipfile index bdaa3fd..456e395 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ verify_ssl = true [dev-packages] [packages] -ephem = "*" +skyfield = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 18f0f32..58e78d3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "522d4a9be67c6920ba1cc2d46f4ac93658a70bf4f0c51611fe4425b6785f27c0" + "sha256": "a4606ffb65d1c5bf46146e7c9cd8add1593934c7536c007bfd84a9f10427258e" }, "pipfile-spec": 6, "requires": { @@ -16,20 +16,52 @@ ] }, "default": { - "ephem": { + "jplephem": { "hashes": [ - "sha256:3884de133045d2f12784ef456c0f5557139a247b88d2c26097f7bd420803ed7f", - "sha256:4bcd9899863ef04f4e75d894a6973dce4b4d16baeb8c2e96fb66bd3c677491a2", - "sha256:6a2e445ba3a1e6bd9d6dedcafa4dda83957f4f9b0efac3d642974c55faebcfa4", - "sha256:7a4c82b1def2893e02aec0394f108d24adb17bd7b0ca6f4bc78eb7120c0212ac", - "sha256:7af6d726c3d903087c284e3dd72c5cda2b5438e84f2d564314469f0fb7494fab", - "sha256:9ea5c8d9b407fe151cece238d13e3ca12114ac5c73269ef6541bf65b208048a3", - "sha256:bb3e04e981352ab8c6049325533944b882d9be1bf13c19be5d85b918ba75723f", - "sha256:f19a380f83f36e56e6e499bf673a43c42ed28c766c9cafb4326b5defcca0a116", - "sha256:fd15421938cac27cd87c3b73c81e4695ab7a22cd37d43e05090140f5d48392d8" + "sha256:9dffb9f3d3f6d996ade875102431fe385e8ea422da25c8ba17b0508d9ca1282b" + ], + "version": "==2.9" + }, + "numpy": { + "hashes": [ + "sha256:0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", + "sha256:141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", + "sha256:14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", + "sha256:27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", + "sha256:2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", + "sha256:3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", + "sha256:52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", + "sha256:6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", + "sha256:7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", + "sha256:7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", + "sha256:94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", + "sha256:a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", + "sha256:ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", + "sha256:b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", + "sha256:b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", + "sha256:cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", + "sha256:d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", + "sha256:dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", + "sha256:dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", + "sha256:e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", + "sha256:ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", + "sha256:f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", + "sha256:f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7" + ], + "version": "==1.16.4" + }, + "sgp4": { + "hashes": [ + "sha256:1fb3cdbc11981a9ff34a032169f83c1f4a2877d1b6c295aed044e1d890b73892" + ], + "version": "==1.4" + }, + "skyfield": { + "hashes": [ + "sha256:7711838214a23ba09bec0bc0c8040ba18dab58f4d496f5be66cf00b56e63ec34" ], "index": "pypi", - "version": "==3.7.6.0" + "version": "==1.10" } }, "develop": {} diff --git a/ephemeris.py b/ephemeris.py new file mode 100644 index 0000000..b80c581 --- /dev/null +++ b/ephemeris.py @@ -0,0 +1,100 @@ +from skyfield.api import Loader, Topos +from skyfield import almanac + + +class Ephemeris: + position = None + timescale = None + planets = None + + def __init__(self, position): + self.MONTH = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] + self.PLANETS = ['mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune', 'pluto'] + + load = Loader('./cache') + self.timescale = load.timescale() + self.planets = load('de421.bsp') + + 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) + + return ephemeris + + def get_sun(self, time1, time2) -> dict: + t, y = almanac.find_discrete(time1, time2, almanac.sunrise_sunset(self.planets, self.position)) + + sunrise = t[0] if y[0] else t[1] + sunset = t[1] if not y[1] else t[0] + + return {'rise': sunrise.utc_iso(), 'set': sunset.utc_iso()} + + def get_moon(self, year, month, day) -> dict: + time1 = self.timescale.utc(year, month, day - 10) + time2 = self.timescale.utc(year, month, day) + + _, y = almanac.find_discrete(time1, time2, almanac.moon_phases(self.planets)) + + return {'phase': y[-1]} + + def compute_ephemeris_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 + + e = [] + + for day in range(1, max_day + 1): + e.append(self.compute_ephemeris_for_day(year, month, day)) + + return e + + def compute_ephemeris_for_year(self, year: int) -> dict: + e = {} + for month in range(0, 12): + e[self.MONTH[month]] = self.compute_ephemeris_for_month(year, month + 1) + + e['seasons'] = self.get_seasons(year) + + return e + + def get_seasons(self, year: int) -> dict: + t1 = self.timescale.utc(year, 1, 1) + t2 = self.timescale.utc(year, 12, 31) + t, y = almanac.find_discrete(t1, t2, almanac.seasons(self.planets)) + + seasons = {} + for ti, yi in zip(t, y): + if yi == 0: + season = 'MARCH' + elif yi == 1: + season = 'JUNE' + elif yi == 2: + season = 'SEPTEMBER' + elif yi == 3: + season = 'DECEMBER' + else: + raise AssertionError + + seasons[season] = ti.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) + elif month is not None: + return self.compute_ephemeris_for_month(year, month) + else: + return self.compute_ephemeris_for_year(year) diff --git a/main.py b/main.py index 0bb3bbc..22d04f6 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,54 @@ -import sys -import ephem +import argparse +import numpy +from datetime import date +from ephemeris import Ephemeris +import json + + +# Fixes the "TypeError: Object of type int64 is not JSON serializable" +# See https://stackoverflow.com/a/50577730 +def json_default(o): + if isinstance(o, numpy.int64): + return int(o) + raise TypeError('Object of type ' + str(type(o)) + ' could not be integrated in the JSON') + + +def main(): + args = get_args() + year = args.year + month = args.month + day = args.date + position = {'lat': args.latitude, 'lon': args.longitude, 'altitude': args.altitude} + + if day is not None and month is None: + month = date.today().month + + ephemeris = Ephemeris(position) + e = ephemeris.compute_ephemeris(year, month, day) + + print(json.dumps(e, default=json_default, indent=4, separators=(',', ': '))) + + +def get_args(): + parser = argparse.ArgumentParser(description='Compute the ephemeris for a given day/month/year, by default for' + ' Paris, France.', 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') + + return parser.parse_args() -def main(argv): - print(argv) if '__main__' == __name__: - main(sys.argv) + main()