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