Browse Source

Merge pull request #27 from Deuchnord/enhance-moon-phase

Compute more accurate Moon phase
tags/v0.3.0
Jérôme Deuchnord 4 years ago
committed by GitHub
parent
commit
d7901bce7a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 18 deletions
  1. +44
    -1
      kosmorrolib/core.py
  2. +29
    -4
      kosmorrolib/data.py
  3. +11
    -3
      kosmorrolib/dumper.py
  4. +23
    -7
      kosmorrolib/ephemerides.py
  5. +8
    -3
      test/dumper.py
  6. +64
    -0
      test/ephemerides.py

+ 44
- 1
kosmorrolib/core.py View File

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

+ 29
- 4
kosmorrolib/data.py View File

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


+ 11
- 3
kosmorrolib/dumper.py View File

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

+ 23
- 7
kosmorrolib/ephemerides.py View File

@@ -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]}



+ 8
- 3
test/dumper.py View File

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



+ 64
- 0
test/ephemerides.py View File

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

Loading…
Cancel
Save