Compute more accurate Moon phasetags/v0.3.0
@@ -18,9 +18,13 @@ | |||
from shutil import rmtree | |||
from pathlib import Path | |||
from typing import Union | |||
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' | |||
@@ -50,5 +54,44 @@ def get_skf_objects(): | |||
return get_loader()('de421.bsp') | |||
def get_iau2000b(time: Time): | |||
return iau2000b(time.tt) | |||
def clear_cache(): | |||
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 = { | |||
'NEW_MOON': 'New Moon', | |||
'WAXING_CRESCENT': 'Waxing crescent', | |||
'FIRST_QUARTER': 'First Quarter', | |||
'WAXING_GIBBOUS': 'Waxing gibbous', | |||
'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: | |||
@@ -22,7 +22,7 @@ import json | |||
from tabulate import tabulate | |||
from skyfield.timelib import Time | |||
from numpy import int64 | |||
from .data import Object, AsterEphemerides, MOON_PHASES | |||
from .data import Object, AsterEphemerides, MoonPhase | |||
class Dumper(ABC): | |||
@@ -55,6 +55,11 @@ class JsonDumper(Dumper): | |||
return obj | |||
if isinstance(obj, AsterEphemerides): | |||
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))) | |||
@@ -94,5 +99,8 @@ class TextDumper(Dumper): | |||
stralign='center', colalign=('left',)) | |||
@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/>. | |||
import datetime | |||
from skyfield import almanac | |||
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 | |||
@@ -42,13 +44,27 @@ class EphemeridesComputer: | |||
return {'rise': sunrise, 'set': sunset} | |||
@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) | |||
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 | |||
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) | |||
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) | |||
for aster in ASTERS]} | |||
@@ -1,13 +1,18 @@ | |||
import unittest | |||
from kosmorrolib.data import AsterEphemerides, Planet | |||
from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase | |||
from kosmorrolib.dumper import JsonDumper | |||
from kosmorrolib.core import get_timescale | |||
class DumperTestCase(unittest.TestCase): | |||
def test_json_dumper_returns_correct_json(self): | |||
data = self._get_data() | |||
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' | |||
' {\n' | |||
' "name": "Mars",\n' | |||
@@ -23,7 +28,7 @@ class DumperTestCase(unittest.TestCase): | |||
@staticmethod | |||
def _get_data(): | |||
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))] | |||
} | |||
@@ -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-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__': | |||
unittest.main() |