Compute more accurate Moon phasetags/v0.3.0
@@ -18,9 +18,13 @@ | |||||
from shutil import rmtree | from shutil import rmtree | ||||
from pathlib import Path | from pathlib import Path | ||||
from typing import Union | |||||
from skyfield.api import Loader | from skyfield.api import Loader | ||||
from skyfield.timelib import Time | |||||
from skyfield.nutationlib import iau2000b | |||||
from .data import Star, Planet, Satellite | |||||
from .data import Star, Planet, Satellite, MOON_PHASES, MoonPhase | |||||
CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' | CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' | ||||
@@ -50,5 +54,44 @@ def get_skf_objects(): | |||||
return get_loader()('de421.bsp') | return get_loader()('de421.bsp') | ||||
def get_iau2000b(time: Time): | |||||
return iau2000b(time.tt) | |||||
def clear_cache(): | def clear_cache(): | ||||
rmtree(CACHE_FOLDER) | rmtree(CACHE_FOLDER) | ||||
def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]: | |||||
tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1) | |||||
phases = list(MOON_PHASES.keys()) | |||||
current_phase = None | |||||
current_phase_time = None | |||||
next_phase_time = None | |||||
i = 0 | |||||
if len(times) == 0: | |||||
return None | |||||
for i, time in enumerate(times): | |||||
if now.utc_iso() <= time.utc_iso(): | |||||
if vals[i] in [0, 2, 4, 6]: | |||||
if time.utc_datetime() < tomorrow.utc_datetime(): | |||||
current_phase_time = time | |||||
current_phase = phases[vals[i]] | |||||
else: | |||||
i -= 1 | |||||
current_phase_time = None | |||||
current_phase = phases[vals[i]] | |||||
else: | |||||
current_phase = phases[vals[i]] | |||||
break | |||||
for j in range(i + 1, len(times)): | |||||
if vals[j] in [0, 2, 4, 6]: | |||||
next_phase_time = times[j] | |||||
break | |||||
return MoonPhase(current_phase, current_phase_time, next_phase_time) |
@@ -24,15 +24,40 @@ from skyfield.timelib import Time | |||||
MOON_PHASES = { | MOON_PHASES = { | ||||
'NEW_MOON': 'New Moon', | 'NEW_MOON': 'New Moon', | ||||
'WAXING_CRESCENT': 'Waxing crescent', | |||||
'FIRST_QUARTER': 'First Quarter', | 'FIRST_QUARTER': 'First Quarter', | ||||
'WAXING_GIBBOUS': 'Waxing gibbous', | |||||
'FULL_MOON': 'Full Moon', | 'FULL_MOON': 'Full Moon', | ||||
'LAST_QUARTER': 'Last Quarter' | |||||
'WANING_GIBBOUS': 'Waning gibbous', | |||||
'LAST_QUARTER': 'Last Quarter', | |||||
'WANING_CRESCENT': 'Waning crescent' | |||||
} | } | ||||
def skyfield_to_moon_phase(val: int) -> str: | |||||
phases = list(MOON_PHASES.keys()) | |||||
return phases[val] | |||||
class MoonPhase: | |||||
def __init__(self, identifier: str, time: Union[Time, None], next_phase_date: Union[Time, None]): | |||||
if identifier not in MOON_PHASES.keys(): | |||||
raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()), | |||||
identifier)) | |||||
self.identifier = identifier | |||||
self.time = time | |||||
self.next_phase_date = next_phase_date | |||||
def get_phase(self): | |||||
return MOON_PHASES[self.identifier] | |||||
def get_next_phase(self): | |||||
if self.identifier == 'NEW_MOON': | |||||
next_identifier = 'FIRST_QUARTER' | |||||
elif self.identifier == 'FIRST_QUARTER': | |||||
next_identifier = 'FULL_MOON' | |||||
elif self.identifier == 'FULL_MOON': | |||||
next_identifier = 'LAST_QUARTER' | |||||
else: | |||||
next_identifier = 'NEW_MOON' | |||||
return MOON_PHASES[next_identifier] | |||||
class Position: | class Position: | ||||
@@ -22,7 +22,7 @@ import json | |||||
from tabulate import tabulate | from tabulate import tabulate | ||||
from skyfield.timelib import Time | from skyfield.timelib import Time | ||||
from numpy import int64 | from numpy import int64 | ||||
from .data import Object, AsterEphemerides, MOON_PHASES | |||||
from .data import Object, AsterEphemerides, MoonPhase | |||||
class Dumper(ABC): | class Dumper(ABC): | ||||
@@ -55,6 +55,11 @@ class JsonDumper(Dumper): | |||||
return obj | return obj | ||||
if isinstance(obj, AsterEphemerides): | if isinstance(obj, AsterEphemerides): | ||||
return obj.__dict__ | return obj.__dict__ | ||||
if isinstance(obj, MoonPhase): | |||||
moon_phase = obj.__dict__ | |||||
moon_phase['phase'] = moon_phase.pop('identifier') | |||||
moon_phase['date'] = moon_phase.pop('time') | |||||
return moon_phase | |||||
raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj))) | raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj))) | ||||
@@ -94,5 +99,8 @@ class TextDumper(Dumper): | |||||
stralign='center', colalign=('left',)) | stralign='center', colalign=('left',)) | ||||
@staticmethod | @staticmethod | ||||
def get_moon(moon_phase: str) -> str: | |||||
return 'Moon phase: %s' % MOON_PHASES[moon_phase] | |||||
def get_moon(moon_phase: MoonPhase) -> str: | |||||
return 'Moon phase: %s\n' \ | |||||
'%s on %s' % (moon_phase.get_phase(), | |||||
moon_phase.get_next_phase(), | |||||
moon_phase.next_phase_date.utc_strftime('%a %b %-d, %Y %H:%M')) |
@@ -17,11 +17,13 @@ | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | # along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
import datetime | import datetime | ||||
from skyfield import almanac | from skyfield import almanac | ||||
from skyfield.timelib import Time | from skyfield.timelib import Time | ||||
from skyfield.constants import tau | |||||
from .data import Object, Position, AsterEphemerides, skyfield_to_moon_phase | |||||
from .core import get_skf_objects, get_timescale, ASTERS, MONTHS | |||||
from .data import Object, Position, AsterEphemerides, MoonPhase | |||||
from .core import get_skf_objects, get_timescale, get_iau2000b, ASTERS, MONTHS, skyfield_to_moon_phase | |||||
RISEN_ANGLE = -0.8333 | RISEN_ANGLE = -0.8333 | ||||
@@ -42,13 +44,27 @@ class EphemeridesComputer: | |||||
return {'rise': sunrise, 'set': sunset} | return {'rise': sunrise, 'set': sunset} | ||||
@staticmethod | @staticmethod | ||||
def get_moon(year, month, day) -> str: | |||||
def get_moon_phase(year, month, day) -> MoonPhase: | |||||
earth = get_skf_objects()['earth'] | |||||
moon = get_skf_objects()['moon'] | |||||
sun = get_skf_objects()['sun'] | |||||
def moon_phase_at(time: Time): | |||||
time._nutation_angles = get_iau2000b(time) | |||||
current_earth = earth.at(time) | |||||
_, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') | |||||
_, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') | |||||
return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) | |||||
moon_phase_at.rough_period = 7.0 # one lunar phase per week | |||||
today = get_timescale().utc(year, month, day) | |||||
time1 = get_timescale().utc(year, month, day - 10) | time1 = get_timescale().utc(year, month, day - 10) | ||||
time2 = get_timescale().utc(year, month, day) | |||||
time2 = get_timescale().utc(year, month, day + 10) | |||||
_, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects())) | |||||
times, phase = almanac.find_discrete(time1, time2, moon_phase_at) | |||||
return skyfield_to_moon_phase(moon_phase[-1]) | |||||
return skyfield_to_moon_phase(times, phase, today) | |||||
@staticmethod | @staticmethod | ||||
def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | ||||
@@ -89,7 +105,7 @@ class EphemeridesComputer: | |||||
return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) | 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: | def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: | ||||
return {'moon_phase': self.get_moon(year, month, day), | |||||
return {'moon_phase': self.get_moon_phase(year, month, day), | |||||
'details': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) | 'details': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) | ||||
for aster in ASTERS]} | for aster in ASTERS]} | ||||
@@ -1,13 +1,18 @@ | |||||
import unittest | import unittest | ||||
from kosmorrolib.data import AsterEphemerides, Planet | |||||
from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase | |||||
from kosmorrolib.dumper import JsonDumper | from kosmorrolib.dumper import JsonDumper | ||||
from kosmorrolib.core import get_timescale | |||||
class DumperTestCase(unittest.TestCase): | class DumperTestCase(unittest.TestCase): | ||||
def test_json_dumper_returns_correct_json(self): | def test_json_dumper_returns_correct_json(self): | ||||
data = self._get_data() | data = self._get_data() | ||||
self.assertEqual('{\n' | self.assertEqual('{\n' | ||||
' "moon_phase": "FULL_MOON",\n' | |||||
' "moon_phase": {\n' | |||||
' "next_phase_date": "2019-11-20T00:00:00Z",\n' | |||||
' "phase": "FULL_MOON",\n' | |||||
' "date": "2019-11-11T00:00:00Z"\n' | |||||
' },\n' | |||||
' "details": [\n' | ' "details": [\n' | ||||
' {\n' | ' {\n' | ||||
' "name": "Mars",\n' | ' "name": "Mars",\n' | ||||
@@ -23,7 +28,7 @@ class DumperTestCase(unittest.TestCase): | |||||
@staticmethod | @staticmethod | ||||
def _get_data(): | def _get_data(): | ||||
return { | return { | ||||
'moon_phase': 'FULL_MOON', | |||||
'moon_phase': MoonPhase('FULL_MOON', get_timescale().utc(2019, 11, 11), get_timescale().utc(2019, 11, 20)), | |||||
'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))] | 'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))] | ||||
} | } | ||||
@@ -17,6 +17,70 @@ class EphemeridesComputerTestCase(unittest.TestCase): | |||||
self.assertEqual('2019-11-18T11:45:02Z', star.ephemerides.culmination_time.utc_iso()) | self.assertEqual('2019-11-18T11:45:02Z', star.ephemerides.culmination_time.utc_iso()) | ||||
self.assertEqual('2019-11-18T17:48:39Z', star.ephemerides.set_time.utc_iso()) | self.assertEqual('2019-11-18T17:48:39Z', star.ephemerides.set_time.utc_iso()) | ||||
################################################################################################################### | |||||
### MOON PHASE TESTS ### | |||||
################################################################################################################### | |||||
def test_moon_phase_new_moon(self): | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 25) | |||||
self.assertEqual('WANING_CRESCENT', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-26T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 26) | |||||
self.assertEqual('NEW_MOON', phase.identifier) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-12-04T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 27) | |||||
self.assertEqual('WAXING_CRESCENT', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-12-04T') | |||||
def test_moon_phase_first_crescent(self): | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 3) | |||||
self.assertEqual('WAXING_CRESCENT', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-04T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 4) | |||||
self.assertEqual('FIRST_QUARTER', phase.identifier) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-12T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 5) | |||||
self.assertEqual('WAXING_GIBBOUS', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-12T') | |||||
def test_moon_phase_full_moon(self): | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 11) | |||||
self.assertEqual('WAXING_GIBBOUS', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-12T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 12) | |||||
self.assertEqual('FULL_MOON', phase.identifier) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-19T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 13) | |||||
self.assertEqual('WANING_GIBBOUS', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-19T') | |||||
def test_moon_phase_last_quarter(self): | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 18) | |||||
self.assertEqual('WANING_GIBBOUS', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-19T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 19) | |||||
self.assertEqual('LAST_QUARTER', phase.identifier) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-26T') | |||||
phase = EphemeridesComputer.get_moon_phase(2019, 11, 20) | |||||
self.assertEqual('WANING_CRESCENT', phase.identifier) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-26T') | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
unittest.main() | unittest.main() |